小程序初始提交

This commit is contained in:
jdc
2025-11-13 10:36:23 +08:00
parent f26b4f9a2f
commit 5db3b180eb
447 changed files with 83351 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
<template>
<cl-page title="工作建议">
<view class="p-4">
<!-- 范围选择 -->
<!-- 行1范围 + 周/月 -->
<view class="flex items-center gap-2 mb-2">
<text class="label">范围:</text>
<view class="chip" :class="{ active: scope==='week' }" @tap="setScope('week')">周</view>
<view class="chip" :class="{ active: scope==='month' }" @tap="setScope('month')">月</view>
</view>
<!-- 行2日期输入 + 刷新(左侧成组,右侧按钮) -->
<view class="flex items-center justify-between mb-4">
<view class="flex items-center gap-2 flex-1">
<template v-if="scope === 'week'">
<text class="text-gray-500">周起始(周一):</text>
<view class="flex-1">
<cl-input v-model="weekStart" type="date" placeholder="选择本周周一" />
</view>
</template>
<template v-else>
<text class="text-gray-500">月份YYYY-MM</text>
<view class="flex-1">
<cl-input v-model="month" placeholder="例如 2025-11" />
</view>
</template>
</view>
<cl-button type="primary" size="small" style="margin-left: 5px;" @tap="loadAdvice">刷新</cl-button>
</view>
<!-- 建议内容 -->
<view v-if="loading" class="py-12 text-center text-gray-400">加载中...</view>
<view v-else>
<view v-if="adviceText" class="p-4 bg-white rounded-2xl whitespace-pre-wrap">{{ adviceText }}</view>
<view v-else class="p-8 text-center text-gray-400">{{ emptyText }}</view>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { request, useStore } from '@/cool';
import { useUi } from '@/uni_modules/cool-ui';
const ui = useUi();
const { user } = useStore();
const scope = ref<'week' | 'month'>('week');
const weekStart = ref(''); // yyyy-MM-dd周一
const month = ref(''); // yyyy-MM
const adviceText = ref('');
const emptyText = ref('暂无建议,请调整时间范围后重试');
const loading = ref(false);
onMounted(async () => {
if (user.token) { try { await user.get(); } catch {} }
// 默认周:设置为当前周周一;默认月:当前月
const today = new Date();
const monday = new Date(today);
const day = monday.getDay();
const diff = (day === 0 ? -6 : 1 - day); // 周一为 1周日为 0
monday.setDate(monday.getDate() + diff);
weekStart.value = fmtYMD(monday);
month.value = `${monday.getFullYear()}-${String(monday.getMonth() + 1).padStart(2,'0')}`;
loadAdvice();
});
function setScope(s: 'week' | 'month') {
scope.value = s;
}
function fmtYMD(d: Date) {
const Y = d.getFullYear();
const M = String(d.getMonth() + 1).padStart(2, '0');
const D = String(d.getDate()).padStart(2, '0');
return `${Y}-${M}-${D}`;
}
async function loadAdvice() {
try {
loading.value = true;
adviceText.value = '';
if (!user.info.value?.id) {
return ui.showToast({ message: '请先登录', type: 'error' });
}
const params: any = { scope: scope.value, userId: user.info.value.id };
if (scope.value === 'week') {
if (!weekStart.value) return ui.showToast({ message: '请选择周一日期', type: 'error' });
params.startDate = weekStart.value;
} else {
const m = (month.value || '').trim();
if (!/^\d{4}-\d{2}$/.test(m)) return ui.showToast({ message: '请输入有效月份,如 2025-11', type: 'error' });
params.startDate = `${m}-01`;
}
const res = await request({ url: '/app/useradvice/advice', method: 'GET', params });
adviceText.value = res?.adviceText || '';
if (!adviceText.value) {
emptyText.value = scope.value === 'month'
? '未查询到该月份的月报/周报/日报内容,请核实后再尝试'
: '未查询到该周的可用内容,请核实后再尝试';
}
} catch (e: any) {
adviceText.value = '';
emptyText.value = e?.message || (scope.value === 'month'
? '未查询到该月份的月报/周报/日报内容,请核实后再尝试'
: '未查询到该周的可用内容,请核实后再尝试');
} finally {
loading.value = false;
}
}
</script>
<style scoped>
.label { color: #6b7280; /* gray-500 */ margin-right: 8px; }
.chip {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 9999px;
border: 1px solid #d1d5db; /* gray-300 */
color: #374151; /* gray-700 */
}
.chip.active {
background: #3b82f6; /* blue-500 */
border-color: #3b82f6;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<cl-page title="日报详情">
<view v-if="loading" class="flex justify-center items-center py-20">
<cl-loading />
</view>
<view v-else-if="report" class="p-4">
<!-- 头部信息 -->
<view class="mb-6">
<view class="flex justify-between items-center mb-2">
<text class="text-2xl font-bold">{{ formatDate(report.reportDate) }}</text>
<cl-tag :type="report.status === 1 ? 'success' : 'warning'">
{{ report.status === 1 ? '已提交' : '草稿' }}
</cl-tag>
</view>
<text class="text-sm text-gray-500">
{{ report.inputType === 1 ? "🎤 语音输入" : "⌨️ 文字输入" }} ·
{{ report.submitTime ? ('提交于 ' + formatDateTime(report.submitTime)) : ('创建于 ' + formatDateTime(report.createTime)) }}
</text>
</view>
<!-- 最终日报内容 -->
<view class="mb-6">
<text class="text-base font-bold mb-3 block">📝 日报内容</text>
<view class="p-4 bg-white rounded-lg shadow-sm">
<text class="text-sm text-gray-800">{{ report.userEditedContent || "暂无内容" }}</text>
</view>
</view>
<!-- 折叠面板查看原始内容和AI生成内容 -->
<cl-collapse v-model="activeNames">
<!-- 原始内容 -->
<cl-collapse-item v-if="report.originalText" name="original" title="📄 原始输入内容">
<view class="p-3 bg-gray-50 rounded">
<text class="text-sm text-gray-700">{{ report.originalText }}</text>
</view>
</cl-collapse-item>
<!-- AI格式化内容 -->
<cl-collapse-item v-if="report.aiFormattedContent" name="ai" title="🤖 AI生成内容">
<view class="p-3 bg-blue-50 rounded">
<text class="text-sm text-gray-800">{{ report.aiFormattedContent }}</text>
</view>
</cl-collapse-item>
</cl-collapse>
<!-- 操作按钮 -->
<view class="flex gap-3 mt-6">
<cl-button type="primary" size="large" :flex="1" @tap="editReport">
编辑日报
</cl-button>
<cl-button v-if="report.status === 0" type="success" size="large" :flex="1" @tap="submitReport" :loading="isSubmitting">
{{ isSubmitting ? "提交中..." : "提交日报" }}
</cl-button>
</view>
</view>
<view v-else class="flex flex-col items-center justify-center py-20">
<text class="text-6xl mb-4">📝</text>
<text class="text-gray-400 text-base">日报不存在</text>
<cl-button type="primary" size="small" class="mt-4" @tap="router.back()">
返回列表
</cl-button>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { request, router, useStore } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
import { onLoad } from "@dcloudio/uni-app";
const ui = useUi();
const { user } = useStore();
// 日报数据
const report = ref<any>(null);
const loading = ref(true);
const isSubmitting = ref(false);
// 折叠面板展开项
const activeNames = ref<string[]>([]);
// 用户ID从登录状态获取
const userId = ref(0);
// 路由参数onLoad 中获取)
const reportId = ref("");
const reportDateParam = ref("");
onLoad(async (options: any) => {
// 读取路由参数
reportId.value = (options?.id ?? "").toString();
reportDateParam.value = (options?.date ?? "").toString();
// 刷新用户信息
if (user.token) {
try { await user.get(); } catch (e) { console.error("【日报详情】获取用户信息失败:", e); }
}
// 获取当前登录用户ID
if (user.info.value && user.info.value.id) {
userId.value = user.info.value.id;
} else {
ui.showToast({ message: "请先登录", type: "error" });
setTimeout(() => { router.to("/pages/user/login"); }, 1000);
loading.value = false;
return;
}
if (reportId.value) {
await loadReportDetail();
} else if (reportDateParam.value) {
await loadReportDetailByDateFallback();
} else {
loading.value = false;
}
});
// 加载日报详情按ID
async function loadReportDetail() {
loading.value = true;
try {
const res = await request({
url: "/app/dailyreport/report/detail",
method: "GET",
params: { id: reportId.value, userId: userId.value }
});
if (res) { report.value = res; return; }
await loadReportDetailByDateFallback();
} catch (error: any) {
console.error("加载日报详情失败:", error);
await loadReportDetailByDateFallback();
} finally {
loading.value = false;
}
}
// 按日期回退查询
async function loadReportDetailByDateFallback() {
if (!reportDateParam.value) return;
loading.value = true;
try {
const res = await request({
url: "/app/dailyreport/report/myReports",
method: "GET",
params: {
userId: userId.value,
startDate: reportDateParam.value,
endDate: reportDateParam.value
}
});
if (Array.isArray(res) && res.length > 0) {
report.value = res[0];
}
} catch (error: any) {
console.error("按日期加载日报失败:", error);
} finally {
loading.value = false;
}
}
// 编辑日报
function editReport() {
const id = reportId.value || report.value?.id;
if (id) {
router.to(`/pages/dailyreport/submit?id=${id}`);
} else if (reportDateParam.value) {
router.to(`/pages/dailyreport/submit?date=${reportDateParam.value}`);
} else {
ui.showToast({ message: "无有效日报标识", type: "warn" });
}
}
// 提交日报(草稿转提交)
async function submitReport() {
if (!report.value) return;
isSubmitting.value = true;
try {
await request({
url: "/app/dailyreport/report/submit",
method: "POST",
data: {
userId: userId.value,
reportDate: report.value.reportDate,
originalText: report.value.originalText,
aiFormattedContent: report.value.aiFormattedContent,
userEditedContent: report.value.userEditedContent,
inputType: report.value.inputType
}
});
ui.showToast({ message: "日报提交成功", type: "success" });
setTimeout(() => { loadReportDetail(); }, 500);
} catch (error: any) {
console.error("提交日报失败:", error);
ui.showToast({ message: "提交失败: " + (error.message || "未知错误"), type: "error" });
} finally {
isSubmitting.value = false;
}
}
// 工具:日期与时间格式化
function formatDate(dateStr: string) {
if (!dateStr) return "";
const d = new Date(dateStr);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${dd}`;
}
function formatDateTime(timeStr: string) {
if (!timeStr) return "";
const date = new Date(timeStr);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
return `${y}-${m}-${d} ${hh}:${mm}`;
}
</script>

View File

@@ -0,0 +1,327 @@
<template>
<cl-page title="我的日报">
<!-- 筛选栏 -->
<view class="px-4 py-3 bg-white border-b">
<view class="flex gap-2">
<cl-button
:type="filterType === 'all' ? 'primary' : 'default'"
size="small"
@tap="changeFilter('all')"
>
全部
</cl-button>
<cl-button
:type="filterType === 'draft' ? 'primary' : 'default'"
size="small"
@tap="changeFilter('draft')"
>
草稿
</cl-button>
<cl-button
:type="filterType === 'submitted' ? 'primary' : 'default'"
size="small"
@tap="changeFilter('submitted')"
>
已提交
</cl-button>
</view>
</view>
<!-- 日报列表 -->
<scroll-view scroll-y class="flex-1">
<!-- 加载中 -->
<view v-if="loading && list.length === 0" class="flex items-center justify-center py-20">
<text class="text-gray-400">加载中...</text>
</view>
<!-- 列表内容 -->
<view v-else-if="list.length > 0">
<view
v-for="(item, index) in list"
:key="item.id || index"
class="mx-4 my-2 p-4 bg-white rounded-lg shadow-sm"
@tap="toDetail(item)"
>
<!-- 头部:日期和状态 -->
<view class="flex justify-between items-center mb-3">
<view class="flex items-center gap-2">
<text class="text-lg font-bold">{{ formatDate(item.reportDate) }}</text>
<text class="text-xs text-gray-500">
{{ formatWeekday(item.reportDate) }}
</text>
</view>
<view
:class="[
'px-2 py-1 rounded text-xs',
item.status === 1 ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
]"
>
{{ item.status === 1 ? "已提交" : "草稿" }}
</view>
</view>
<!-- 内容摘要 -->
<view class="mb-3">
<text class="text-sm text-gray-700">
{{ getContentPreview(item.userEditedContent) }}
</text>
</view>
<!-- 底部信息 -->
<view class="flex justify-between items-center text-xs text-gray-500">
<view class="flex items-center gap-2">
<text>{{ item.inputType === 1 ? "🎤 语音输入" : "⌨️ 文字输入" }}</text>
</view>
<text v-if="item.submitTime">
提交于 {{ formatTime(item.submitTime) }}
</text>
<text v-else-if="item.createTime">
创建于 {{ formatTime(item.createTime) }}
</text>
</view>
</view>
<!-- 加载更多提示 -->
<view v-if="loading" class="flex items-center justify-center py-4">
<text class="text-gray-400 text-sm">加载中...</text>
</view>
<view v-else-if="finished" class="flex items-center justify-center py-4">
<text class="text-gray-400 text-sm">没有更多了</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="flex flex-col items-center justify-center py-20">
<text class="text-6xl mb-4">📝</text>
<text class="text-gray-400 text-base">暂无日报</text>
<view
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
@tap="toSubmit"
>
<text>去提交日报</text>
</view>
</view>
</scroll-view>
<!-- 悬浮提交按钮 -->
<view class="fixed bottom-20 right-4">
<cl-button
type="primary"
size="large"
round
@tap="toSubmit"
>
<text class="text-2xl">✏️</text>
</cl-button>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { request, router, useStore } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const { user } = useStore();
// 列表相关
const list = ref<any[]>([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1);
const pageSize = ref(20);
// 筛选类型
const filterType = ref<"all" | "draft" | "submitted">("all");
// 用户ID从登录状态获取
const userId = ref(0);
// 初始化
onMounted(async () => {
console.log("【日报列表】页面加载, user.info:", user.info.value);
// 先尝试获取最新用户信息
if (user.token) {
try {
await user.get();
} catch (e) {
console.error("【日报列表】获取用户信息失败:", e);
}
}
console.log("【日报列表】获取用户信息后, user.info:", user.info.value);
if (user.info.value && user.info.value.id) {
userId.value = user.info.value.id;
console.log("【日报列表】设置userId:", userId.value);
loadReports();
} else {
console.error("【日报列表】用户未登录或用户信息为空");
ui.showToast({
message: "请先登录",
type: "error"
});
setTimeout(() => {
router.to("/pages/user/login");
}, 1000);
}
});
// 加载日报列表
async function loadReports() {
if (loading.value || finished.value) return;
loading.value = true;
try {
console.log("【日报列表】开始加载, userId:", userId.value);
if (!userId.value || userId.value === 0) {
console.error("【日报列表】userId无效:", userId.value);
throw new Error("用户ID无效请重新登录");
}
const params: any = {
userId: userId.value,
page: page.value,
size: pageSize.value
};
// 添加状态筛选
if (filterType.value === "draft") {
params.status = 0;
} else if (filterType.value === "submitted") {
params.status = 1;
}
console.log("【日报列表】请求参数:", params);
const res = await request({
url: "/app/dailyreport/report/myReports",
method: "GET",
params
});
console.log("【日报列表】响应数据:", res);
console.log("【日报列表】响应类型:", typeof res);
console.log("【日报列表】是否为数组:", Array.isArray(res));
console.log("【日报列表】数据长度:", res ? res.length : 0);
if (res && res.length > 0) {
console.log("【日报列表】开始填充列表数据");
if (page.value === 1) {
list.value = res;
} else {
list.value.push(...res);
}
page.value++;
console.log("【日报列表】列表已更新, 当前list长度:", list.value.length);
console.log("【日报列表】列表第一项数据:", list.value[0]);
// 如果返回数据少于每页大小,说明已经到底了
if (res.length < pageSize.value) {
finished.value = true;
}
} else {
console.log("【日报列表】无数据或数据为空");
// 即使没有数据也不应该标记为finished可能只是筛选条件没有匹配的数据
if (page.value === 1) {
list.value = [];
}
finished.value = true;
}
} catch (error: any) {
console.error("加载日报列表失败:", error);
ui.showToast({
message: "加载失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
loading.value = false;
}
}
// 下拉刷新
function onRefresh() {
console.log("【日报列表】下拉刷新");
page.value = 1;
finished.value = false;
list.value = [];
loadReports();
}
// 切换筛选条件
function changeFilter(type: "all" | "draft" | "submitted") {
console.log("【日报列表】切换筛选:", type);
filterType.value = type;
page.value = 1;
finished.value = false;
list.value = [];
loadReports();
}
// 跳转到详情
function toDetail(item: any) {
const date = item.reportDate ? encodeURIComponent(item.reportDate) : '';
router.to(`/pages/dailyreport/detail?id=${item.id}&date=${date}`);
}
// 跳转到提交页面
function toSubmit() {
router.to("/pages/dailyreport/submit");
}
// 格式化日期
function formatDate(dateStr: string) {
if (!dateStr) return "";
const date = new Date(dateStr);
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${month}月${day}日`;
}
// 格式化星期
function formatWeekday(dateStr: string) {
if (!dateStr) return "";
const date = new Date(dateStr);
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
return weekdays[date.getDay()];
}
// 格式化时间
function formatTime(timeStr: string) {
if (!timeStr) return "";
const date = new Date(timeStr);
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${month}-${day} ${hours}:${minutes}`;
}
// 获取内容摘要
function getContentPreview(content: string) {
if (!content) return "(无内容)";
// 移除 Markdown 标记
const plain = content.replace(/[#*`\[\]()]/g, "").trim();
// 限制长度
return plain.length > 100 ? plain.substring(0, 100) + "..." : plain;
}
</script>
<style scoped>
.gap-2 {
gap: 0.5rem;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,555 @@
<template>
<cl-page title="提交日报">
<view class="p-4">
<!-- 日期选择 -->
<view class="mb-4">
<text class="text-base font-bold mb-2">日报日期</text>
<cl-input
v-model="form.reportDate"
type="date"
placeholder="选择日报日期"
:max="todayDate"
/>
</view>
<!-- 输入方式选择 -->
<view class="mb-4">
<text class="text-base font-bold mb-2">输入方式</text>
<view class="flex gap-2">
<cl-button
:type="inputType === 0 ? 'primary' : 'default'"
size="small"
@tap="inputType = 0"
>
文字输入
</cl-button>
<cl-button
:type="inputType === 1 ? 'primary' : 'default'"
size="small"
@tap="inputType = 1"
>
语音输入
</cl-button>
</view>
</view>
<!-- 语音输入区域 -->
<view v-if="inputType === 1" class="mb-4">
<text class="text-base font-bold mb-2">语音录制</text>
<view class="flex flex-col items-center p-6 bg-gray-50 rounded-lg">
<view v-if="!isRecording && !audioFile" class="flex flex-col items-center">
<text class="text-gray-500 mb-4">按住按钮开始录音最长60秒</text>
<cl-button
type="primary"
size="large"
round
@touchstart="startRecording"
@touchend="stopRecording"
>
<text class="text-2xl">🎤</text>
</cl-button>
</view>
<view v-if="isRecording" class="flex flex-col items-center">
<text class="text-red-500 text-lg mb-2">录音中...</text>
<text class="text-3xl font-bold text-red-500 mb-4">{{ recordingTime }}s</text>
<cl-button type="danger" @tap="cancelRecording">取消录音</cl-button>
</view>
<view v-if="audioFile && !isRecording" class="flex flex-col items-center w-full">
<text class="text-green-500 mb-2">录音完成</text>
<text class="text-gray-600 mb-4">时长: {{ recordingTime }}秒</text>
<view class="flex gap-2">
<cl-button type="default" @tap="reRecord">重新录音</cl-button>
<cl-button type="primary" @tap="recognizeVoice" :loading="isRecognizing">
{{ isRecognizing ? '识别中...' : '语音识别' }}
</cl-button>
</view>
</view>
</view>
<!-- 识别结果 -->
<view v-if="recognizedText" class="mt-4">
<text class="text-base font-bold mb-2">识别结果</text>
<view class="p-3 bg-blue-50 rounded-lg">
<text class="text-gray-700">{{ recognizedText }}</text>
</view>
</view>
</view>
<!-- 文字输入区域 -->
<view v-if="inputType === 0" class="mb-4">
<view class="p-4 bg-white rounded-2xl">
<cl-form>
<cl-form-item label="工作内容" required>
<cl-input
v-model="originalText"
placeholder="请输入今天的工作内容"
:maxlength="1000"
/>
</cl-form-item>
</cl-form>
</view>
</view>
<!-- AI格式化按钮 -->
<view v-if="originalText || recognizedText" class="mb-4">
<cl-button
type="success"
size="large"
block
@tap="formatWithAI"
:loading="isFormatting"
>
{{ isFormatting ? 'AI生成中...' : '🤖 AI生成日报' }}
</cl-button>
</view>
<!-- AI生成的日报内容 -->
<view v-if="aiFormattedContent" class="mb-4">
<text class="text-base font-bold mb-2">AI生成的日报</text>
<view class="p-3 bg-green-50 rounded-lg mb-2">
<cl-markdown :content="aiFormattedContent" />
</view>
<text class="text-sm text-gray-500">您可以在下方编辑最终内容</text>
</view>
<!-- 最终编辑区域 -->
<view v-if="aiFormattedContent" class="mb-4">
<view class="p-4 bg-white rounded-2xl">
<cl-form>
<cl-form-item label="最终日报内容" required>
<cl-input
v-model="userEditedContent"
placeholder="请编辑最终的日报内容"
:maxlength="2000"
/>
</cl-form-item>
</cl-form>
</view>
</view>
<!-- 操作按钮 -->
<view v-if="userEditedContent" class="flex gap-2 mt-6">
<cl-button
type="default"
size="large"
:flex="1"
@tap="saveDraft"
:loading="isSavingDraft"
>
{{ isSavingDraft ? '保存中...' : '保存草稿' }}
</cl-button>
<cl-button
type="primary"
size="large"
:flex="1"
@tap="submitReport"
:loading="isSubmitting"
>
{{ isSubmitting ? '提交中...' : '提交日报' }}
</cl-button>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { request, router, useStore } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const { user } = useStore();
// 表单数据
const form = ref({
reportDate: "",
userId: 0 // 将在onMounted中从登录用户信息获取
});
// 输入方式0-文字1-语音
const inputType = ref<number>(0);
// 语音录制相关
const isRecording = ref(false);
const recordingTime = ref(0);
const audioFile = ref<string | null>(null);
const recorderManager = ref<any>(null);
let recordTimer: number | null = null;
// 识别和格式化相关
const isRecognizing = ref(false);
const recognizedText = ref("");
const originalText = ref("");
const isFormatting = ref(false);
const aiFormattedContent = ref("");
const userEditedContent = ref("");
// 提交相关
const isSavingDraft = ref(false);
const isSubmitting = ref(false);
// 今天的日期
const todayDate = computed(() => {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
});
onMounted(async () => {
console.log("【日报提交】页面加载, user.info:", user.info.value);
// 先尝试获取最新用户信息
if (user.token) {
try {
await user.get();
} catch (e) {
console.error("【日报提交】获取用户信息失败:", e);
}
}
console.log("【日报提交】获取用户信息后, user.info:", user.info.value);
// 获取当前登录用户ID
if (user.info.value && user.info.value.id) {
form.value.userId = user.info.value.id;
console.log("【日报提交】设置userId:", form.value.userId);
} else {
// 如果未登录,跳转到登录页
console.error("【日报提交】用户未登录或用户信息为空");
ui.showToast({
message: "请先登录",
type: "error"
});
setTimeout(() => {
router.to("/pages/user/login");
}, 1000);
return;
}
// 设置默认日期为今天
form.value.reportDate = todayDate.value;
// 初始化录音管理器(仅在支持的平台)
try {
// @ts-ignore
const manager = uni.getRecorderManager();
if (manager) {
recorderManager.value = manager;
// 监听录音结束
recorderManager.value.onStop((res: any) => {
if (recordTimer) {
clearInterval(recordTimer);
recordTimer = null;
}
audioFile.value = res.tempFilePath;
isRecording.value = false;
});
// 监听录音错误
recorderManager.value.onError((err: any) => {
console.error("录音错误:", err);
ui.showToast({
message: "录音失败: " + err.errMsg,
type: "error"
});
isRecording.value = false;
if (recordTimer) {
clearInterval(recordTimer);
recordTimer = null;
}
});
console.log("录音管理器初始化成功");
} else {
console.warn("当前环境不支持录音功能H5环境请使用文字输入");
}
} catch (e) {
console.warn("录音管理器初始化失败:", e);
}
// 检查今天是否已有日报
checkTodayReport();
});
onUnmounted(() => {
if (recordTimer) {
clearInterval(recordTimer);
}
});
// 检查今天是否已有日报
async function checkTodayReport() {
try {
const res = await request({
url: "/app/dailyreport/report/todayReport",
method: "GET",
params: { userId: form.value.userId }
});
if (res && res.id) {
// 今天已有日报,询问是否继续编辑
uni.showModal({
title: "提示",
content: `您今天已经${res.status === 1 ? "提交" : "保存"}了日报,是否继续编辑?`,
success: (modalRes) => {
if (modalRes.confirm) {
// 加载已有日报
loadExistingReport(res);
} else {
// 跳转到列表页
router.back();
}
}
});
}
} catch (error) {
console.log("今天还没有日报,可以新建");
}
}
// 加载已有日报
function loadExistingReport(report: any) {
originalText.value = report.originalText || "";
aiFormattedContent.value = report.aiFormattedContent || "";
userEditedContent.value = report.userEditedContent || "";
inputType.value = report.inputType || 0;
}
// 开始录音
function startRecording() {
// 检查录音管理器是否可用
if (!recorderManager.value) {
return ui.showToast({
message: "当前环境不支持录音功能,请使用文字输入",
type: "error"
});
}
isRecording.value = true;
recordingTime.value = 0;
audioFile.value = null;
recognizedText.value = "";
// 开始录音
recorderManager.value.start({
duration: 60000, // 最长60秒
format: "mp3"
});
// 开始计时
recordTimer = setInterval(() => {
recordingTime.value++;
if (recordingTime.value >= 60) {
stopRecording();
}
}, 1000);
}
// 停止录音
function stopRecording() {
if (isRecording.value && recorderManager.value) {
recorderManager.value.stop();
}
}
// 取消录音
function cancelRecording() {
if (isRecording.value && recorderManager.value) {
recorderManager.value.stop();
audioFile.value = null;
recordingTime.value = 0;
}
}
// 重新录音
function reRecord() {
audioFile.value = null;
recognizedText.value = "";
recordingTime.value = 0;
}
// 语音识别
async function recognizeVoice() {
if (!audioFile.value) {
return ui.showToast({
message: "请先录音",
type: "error"
});
}
isRecognizing.value = true;
try {
// 上传音频文件
const uploadRes = await uni.uploadFile({
url: request.options.baseURL + "/app/dailyreport/report/uploadVoice",
filePath: audioFile.value,
name: "audio",
formData: {
userId: form.value.userId
}
});
const result = JSON.parse(uploadRes.data);
if (result.code === 1000) {
recognizedText.value = result.data;
originalText.value = result.data;
ui.showToast({
message: "识别成功",
type: "success"
});
} else {
throw new Error(result.message || "识别失败");
}
} catch (error: any) {
console.error("语音识别失败:", error);
ui.showToast({
message: "语音识别失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isRecognizing.value = false;
}
}
// AI格式化
async function formatWithAI() {
const text = originalText.value || recognizedText.value;
if (!text) {
return ui.showToast({
message: "请先输入内容或录音",
type: "error"
});
}
isFormatting.value = true;
try {
const res = await request({
url: "/app/dailyreport/report/aiFormat",
method: "POST",
data: {
originalText: text,
reportDate: form.value.reportDate // 传递日报日期
}
});
// 后端返回的是对象 {formattedContent: "内容", length: 数字}
const formattedContent = res.formattedContent || res;
aiFormattedContent.value = formattedContent;
userEditedContent.value = formattedContent;
ui.showToast({
message: "AI生成成功",
type: "success"
});
} catch (error: any) {
console.error("AI格式化失败:", error);
ui.showToast({
message: "AI格式化失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isFormatting.value = false;
}
}
// 保存草稿
async function saveDraft() {
if (!userEditedContent.value) {
return ui.showToast({
message: "请先生成日报内容",
type: "error"
});
}
isSavingDraft.value = true;
try {
await request({
url: "/app/dailyreport/report/saveDraft",
method: "POST",
data: {
userId: form.value.userId,
reportDate: form.value.reportDate,
originalText: originalText.value || recognizedText.value,
aiFormattedContent: aiFormattedContent.value,
userEditedContent: userEditedContent.value,
inputType: inputType.value
}
});
ui.showToast({
message: "草稿保存成功",
type: "success"
});
// 延迟跳转
setTimeout(() => {
router.back();
}, 1000);
} catch (error: any) {
console.error("保存草稿失败:", error);
ui.showToast({
message: "保存草稿失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isSavingDraft.value = false;
}
}
// 提交日报
async function submitReport() {
if (!userEditedContent.value) {
return ui.showToast({
message: "请先生成日报内容",
type: "error"
});
}
isSubmitting.value = true;
try {
await request({
url: "/app/dailyreport/report/submit",
method: "POST",
data: {
userId: form.value.userId,
reportDate: form.value.reportDate,
originalText: originalText.value || recognizedText.value,
aiFormattedContent: aiFormattedContent.value,
userEditedContent: userEditedContent.value,
inputType: inputType.value
}
});
ui.showToast({
message: "日报提交成功",
type: "success"
});
// 延迟跳转
setTimeout(() => {
router.back();
}, 1000);
} catch (error: any) {
console.error("提交日报失败:", error);
ui.showToast({
message: "提交日报失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isSubmitting.value = false;
}
}
</script>
<style scoped>
.gap-2 {
gap: 0.5rem;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-button>{{ t("普通") }}</cl-button>
</demo-item>
<demo-item :label="t('不同类型')">
<view class="flex flex-row flex-wrap mb-2 overflow-visible">
<cl-button type="primary">{{ t("主要") }}</cl-button>
<cl-button type="success">{{ t("成功") }}</cl-button>
<cl-button type="warn">{{ t("警告") }}</cl-button>
</view>
<view class="flex flex-row mb-2 overflow-visible">
<cl-button type="error">{{ t("危险") }}</cl-button>
<cl-button type="info">{{ t("信息") }}</cl-button>
</view>
<view class="flex flex-row overflow-visible">
<cl-button type="light">{{ t("浅色") }}</cl-button>
<cl-button type="dark">{{ t("深色") }}</cl-button>
</view>
</demo-item>
<demo-item :label="t('只显示图标')">
<view class="flex flex-row">
<cl-button type="primary" icon="send-plane-fill"></cl-button>
<cl-button type="error" icon="verified-badge-fill"></cl-button>
<cl-button type="info" icon="edit-fill"></cl-button>
</view>
</demo-item>
<demo-item :label="t('自定义')">
<view class="flex flex-row justify-center mb-5 h-14 items-center">
<cl-button
:type="type"
:size="size"
:text="isText"
:border="isBorder"
:rounded="isRounded"
:loading="isLoading"
:disabled="isDisabled"
:icon="isIcon ? 'send-plane-fill' : ''"
:color="isColor ? '#4286e0' : ''"
:pt="{
className: parseClass([
{
'!bg-transparent': isColor
}
])
}"
>{{ t("自定义") }}</cl-button
>
</view>
<cl-list border>
<view class="p-2">
<cl-tabs
v-model="size"
:height="66"
:list="sizeOptions"
show-slider
fill
></cl-tabs>
</view>
<cl-list-item :label="t('文本模式')">
<cl-switch v-model="isText"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('带边框')">
<cl-switch v-model="isBorder"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('圆角按钮')">
<cl-switch v-model="isRounded"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('带左侧图标')">
<cl-switch v-model="isIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('加载中')">
<cl-switch v-model="isLoading"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import type { ClButtonType, ClTabsItem, Size } from "@/uni_modules/cool-ui";
import { parseClass } from "@/cool";
import { t } from "@/locale";
const type = ref<ClButtonType>("primary");
const isText = ref(false);
const isBorder = ref(false);
const isRounded = ref(false);
const isLoading = ref(false);
const isIcon = ref(false);
const isDisabled = ref(false);
const isColor = ref(false);
const size = ref<Size>("normal");
const sizeOptions = ref<ClTabsItem[]>([
{
label: t("小"),
value: "small"
},
{
label: t("默认"),
value: "normal"
},
{
label: t("大"),
value: "large"
}
]);
</script>

View File

@@ -0,0 +1,94 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('设置颜色')">
<view class="flex flex-row">
<cl-icon name="heart-fill" color="primary" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="success" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="error" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="warn" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="info" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="#428bca" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="purple"></cl-icon>
</view>
</demo-item>
<demo-item :label="t('设置大小')">
<view class="flex flex-row">
<cl-icon name="heart-fill" class="mr-2" :size="50"></cl-icon>
<cl-icon name="heart-fill" class="mr-2" :size="40"></cl-icon>
<cl-icon name="heart-fill" class="mr-2" :size="30"></cl-icon>
<cl-icon name="heart-fill" class="mr-2" :size="20"></cl-icon>
</view>
</demo-item>
<demo-item>
<cl-text>{{ t("集成 iconfont 与 remixicon 图标库,展示部分示例") }}</cl-text>
</demo-item>
<demo-item :label="t('iconfont')">
<cl-row :gutter="10">
<cl-col :span="4" v-for="item in iconfont" :key="item">
<view
class="flex flex-col items-center justify-center h-[100rpx] rounded-lg"
hover-class="opacity-60"
:hover-stay-time="250"
@tap="copy(item)"
>
<cl-icon :name="item"></cl-icon>
</view>
</cl-col>
</cl-row>
</demo-item>
<demo-item :label="t('remixicon')">
<cl-row :gutter="10">
<cl-col :span="4" v-for="item in remixicon" :key="item">
<view
class="flex flex-col items-center justify-center h-[100rpx]"
hover-class="opacity-60"
:hover-stay-time="250"
@tap="copy(item)"
>
<cl-icon :name="item"></cl-icon>
</view>
</cl-col>
</cl-row>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { icons } from "@/icons";
import { forInObject, keys } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
import { t } from "@/locale";
const ui = useUi();
const remixicon = ref<string[]>([]);
const iconfont = ref<string[]>([]);
forInObject(icons, (value, key) => {
if (key == "iconfont") {
iconfont.value = keys(value).slice(0, 100);
} else {
remixicon.value = keys(value).slice(0, 100);
}
});
function copy(data: string) {
uni.setClipboardData({
data,
showToast: false,
success() {
ui.showToast({
message: t("复制成功")
});
}
});
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-image :src="url"></cl-image>
</demo-item>
<demo-item :label="t('不同裁剪')">
<view class="flex flex-row justify-between">
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="aspectFill"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>aspectFill</cl-text
>
</view>
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="aspectFit"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>aspectFit</cl-text
>
</view>
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="heightFix"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>heightFix</cl-text
>
</view>
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="scaleToFill"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>scaleToFill</cl-text
>
</view>
</view>
</demo-item>
<demo-item :label="t('点击可预览')">
<cl-image :src="url" preview></cl-image>
</demo-item>
<demo-item :label="t('失败时显示')">
<cl-image src="url"></cl-image>
</demo-item>
<demo-item :label="t('加载中')">
<cl-image src=""></cl-image>
</demo-item>
<demo-item :label="t('自定义圆角')">
<view class="flex flex-row">
<cl-image
:src="url"
:pt="{
inner: {
className: '!rounded-none'
}
}"
></cl-image>
<cl-image
:src="url"
:pt="{
className: 'ml-3',
inner: {
className: '!rounded-2xl'
}
}"
></cl-image>
<cl-image
:src="url"
:pt="{
className: 'ml-3',
inner: {
className: '!rounded-3xl'
}
}"
></cl-image>
<cl-image
:src="url"
:pt="{
className: 'ml-3',
inner: {
className: '!rounded-full'
}
}"
></cl-image>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
const url = ref("https://unix.cool-js.com/images/demo/avatar.jpg");
</script>

View File

@@ -0,0 +1,72 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<view class="flex flex-row">
<cl-tag>{{ t("标签") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('不同类型')">
<view class="flex flex-row flex-wrap">
<cl-tag type="primary">{{ t("主要") }}</cl-tag>
<cl-tag type="success">{{ t("成功") }}</cl-tag>
<cl-tag type="warn">{{ t("警告") }}</cl-tag>
<cl-tag type="error">{{ t("危险") }}</cl-tag>
<cl-tag type="info">{{ t("信息") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('带图标')">
<view class="flex flex-row">
<cl-tag icon="mail-line">{{ t("邮件") }}</cl-tag>
<cl-tag icon="calendar-line">{{ t("日历") }}</cl-tag>
<cl-tag icon="file-line">{{ t("文件") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('圆角')">
<view class="flex flex-row">
<cl-tag rounded>{{ t("圆角") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('可关闭')">
<view class="flex flex-row">
<cl-tag closable>{{ t("可关闭") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('镂空')">
<view class="flex flex-row flex-wrap">
<cl-tag type="primary" plain>{{ t("主要") }}</cl-tag>
<cl-tag type="success" plain>{{ t("成功") }}</cl-tag>
<cl-tag type="warn" plain>{{ t("警告") }}</cl-tag>
<cl-tag type="error" plain>{{ t("危险") }}</cl-tag>
<cl-tag type="info" plain>{{ t("信息") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('自定义')">
<view class="flex flex-row">
<cl-tag
:pt="{
className: '!bg-sky-200',
text: {
className: '!text-sky-700'
}
}"
>{{ t("自定义颜色") }}</cl-tag
>
<cl-tag :pt="{ className: '!rounded-none' }">{{ t("自定义无圆角") }}</cl-tag>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
</script>

View File

@@ -0,0 +1,103 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-text>云想衣裳花想容,春风拂槛露华浓。</cl-text>
</demo-item>
<demo-item :label="t('自定义颜色')">
<cl-text color="primary">明月松间照,清泉石上流。</cl-text>
<cl-text color="error">举头望明月,低头思故乡。</cl-text>
<cl-text color="success">春眠不觉晓,处处闻啼鸟。</cl-text>
<cl-text color="warn">劝君更尽一杯酒,西出阳关无故人。</cl-text>
<cl-text color="info">孤帆远影碧空尽,唯见长江天际流。</cl-text>
<cl-text
:pt="{
className: 'text-sky-500'
}"
>大漠孤烟直,长河落日圆。</cl-text
>
</demo-item>
<demo-item :label="t('省略号')">
<cl-text ellipsis
>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</cl-text
>
</demo-item>
<demo-item :label="t('多行省略号')">
<cl-text ellipsis :lines="2"
>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。天阶夜色凉如水,卧看牵牛织女星。人生若只如初见,何事秋风悲画扇。山有木兮木有枝,心悦君兮君不知。</cl-text
>
</demo-item>
<demo-item :label="t('字体大小')">
<cl-text
:pt="{
className: 'text-xs'
}"
>text-xs</cl-text
>
<cl-text
:pt="{
className: 'text-sm'
}"
>text-sm</cl-text
>
<cl-text
:pt="{
className: 'text-md'
}"
>text-md</cl-text
>
<cl-text
:pt="{
className: 'text-lg'
}"
>text-lg</cl-text
>
<cl-text
:pt="{
className: 'text-xl'
}"
>text-xl</cl-text
>
</demo-item>
<demo-item :label="t('自定义尺寸')">
<cl-text :size="20">20rpx</cl-text>
<cl-text size="30rpx">30rpx</cl-text>
<cl-text size="15px">15px</cl-text>
</demo-item>
<demo-item :label="t('金额')">
<cl-text type="amount" :value="10000000.0"></cl-text>
</demo-item>
<demo-item :label="t('手机号脱敏')">
<cl-text type="phone" mask value="13800138000"></cl-text>
</demo-item>
<demo-item :label="t('姓名脱敏')">
<cl-text type="name" mask value="张三"></cl-text>
</demo-item>
<demo-item :label="t('邮箱脱敏')">
<cl-text type="email" mask value="example@example.com"></cl-text>
</demo-item>
<demo-item :label="t('银行卡脱敏')">
<cl-text type="card" mask value="1234 5678 9012 3456"></cl-text>
</demo-item>
<demo-item :label="t('自定义脱敏字符')">
<cl-text type="phone" value="13800138000" mask mask-char="~"></cl-text>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,32 @@
<template>
<view class="p-3 pb-0">
<view class="w-full p-3 bg-white rounded-xl dark:bg-surface-800">
<cl-image :src="item?.image" mode="aspectFill" width="100%" height="280rpx"></cl-image>
<cl-text :pt="{ className: 'mt-2' }">{{ item?.title }}</cl-text>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { parse } from "@/cool";
defineOptions({
name: "goods-item"
});
type GoodsItem = {
id: number;
title: string;
image: string;
};
const props = defineProps({
value: {
type: Object,
default: () => ({})
}
});
const item = computed(() => parse<GoodsItem>(props.value));
</script>

View File

@@ -0,0 +1,28 @@
<template>
<view class="demo-item dark:!bg-surface-800">
<cl-text :pt="{ className: 'text-sm text-surface-400 mb-2' }" v-if="label != ''">{{
label
}}</cl-text>
<slot></slot>
</view>
</template>
<script lang="ts" setup>
defineOptions({
name: "demo-item"
});
const props = defineProps({
label: {
type: String,
default: ""
}
});
</script>
<style lang="scss" scoped>
.demo-item {
@apply p-3 rounded-xl bg-white mb-3;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<view class="bg-surface-100 dark:!bg-surface-700 rounded-lg p-3 mb-3">
<cl-text
:pt="{
className: 'text-sm'
}"
>
<slot></slot>
</cl-text>
</view>
</template>
<script setup lang="ts">
defineOptions({
name: "demo-tips"
});
</script>

View File

@@ -0,0 +1,32 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-avatar src="https://unix.cool-js.com/images/demo/avatar.jpg"></cl-avatar>
</demo-item>
<demo-item :label="t('无图片')">
<cl-avatar></cl-avatar>
</demo-item>
<demo-item :label="t('圆角')">
<cl-avatar
rounded
src="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-avatar>
</demo-item>
<demo-item :label="t('自定义大小')">
<cl-avatar
:size="120"
src="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-avatar>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,59 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-banner :list="list"></cl-banner>
</demo-item>
<demo-item :label="t('禁用手势')">
<cl-banner :list="list" :disable-touch="true"></cl-banner>
</demo-item>
<demo-item :label="t('自定义样式')">
<cl-banner
:list="list"
:pt="{
className: 'mx-[-12rpx] !rounded-none',
item: {
className: parseClass(['scale-y-80 !px-[12rpx]'])
},
itemActive: {
className: parseClass(['!scale-y-100'])
},
image: {
className: '!rounded-none'
}
}"
:previous-margin="40"
:next-margin="40"
></cl-banner>
</demo-item>
<demo-item :label="t('自定义样式2')">
<cl-banner
:list="list"
:pt="{
className: 'mx-[-12rpx]',
item: {
className: parseClass(['px-[12rpx]'])
}
}"
:next-margin="40"
></cl-banner>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { parseClass } from "@/cool";
const list = ref<string[]>([
"https://unix.cool-js.com/images/demo/bg1.png",
"https://unix.cool-js.com/images/demo/bg2.png",
"https://unix.cool-js.com/images/demo/bg3.png"
]);
</script>

View File

@@ -0,0 +1,217 @@
<template>
<cl-page>
<view class="p-3">
<demo-item>
<cl-text color="info">
{{ t("长按项即可拖动排序") }}
</cl-text>
</demo-item>
<demo-item :label="t('单列排序')">
<cl-draggable v-model="list">
<template #item="{ item, index }">
<view
class="flex flex-row items-center p-3 bg-surface-100 rounded-lg mb-2 dark:!bg-surface-700"
:class="{
'opacity-50': item['disabled']
}"
>
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
</view>
</template>
</cl-draggable>
</demo-item>
<demo-item :label="t('不需要长按')">
<cl-draggable v-model="list5" :long-press="false">
<template #item="{ item }">
<view
class="flex flex-row items-center p-3 bg-surface-100 rounded-lg mb-2 dark:!bg-surface-700"
>
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
</view>
</template>
</cl-draggable>
</demo-item>
<demo-item :label="t('结合列表使用')">
<cl-list border>
<cl-draggable v-model="list2">
<template #item="{ item, index, dragging, dragIndex }">
<cl-list-item
icon="chat-thread-line"
:label="(item as UTSJSONObject).label"
arrow
:pt="{
inner: {
className: parseClass([
[
dragging && dragIndex == index,
isDark ? '!bg-surface-700' : '!bg-surface-100'
]
])
}
}"
></cl-list-item>
</template>
</cl-draggable>
</cl-list>
</demo-item>
<demo-item :label="t('多列排序')">
<cl-draggable v-model="list3" :columns="4">
<template #item="{ item, index }">
<view
class="flex flex-row items-center justify-center p-3 bg-surface-100 rounded-lg m-1 dark:!bg-surface-700"
:class="{
'opacity-50': item['disabled']
}"
>
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
</view>
</template>
</cl-draggable>
</demo-item>
<demo-item :label="t('结合图片使用')">
<cl-draggable v-model="list4" :columns="4">
<template #item="{ item, index }">
<view class="p-[2px]">
<cl-image
:src="(item as UTSJSONObject).url"
mode="widthFix"
:pt="{
className: '!w-full'
}"
preview
></cl-image>
</view>
</template>
</cl-draggable>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { isDark, parseClass } from "@/cool";
// list李白《将进酒》
const list = ref<UTSJSONObject[]>([
{
label: "君不见黄河之水天上来"
},
{
label: "奔流到海不复回",
disabled: true
},
{
label: "君不见高堂明镜悲白发"
},
{
label: "朝如青丝暮成雪"
},
{
label: "人生得意须尽欢"
}
]);
// list5杜甫《春望》
const list5 = ref<UTSJSONObject[]>([
{
label: "国破山河在"
},
{
label: "城春草木深"
},
{
label: "感时花溅泪"
}
]);
// list2王之涣《登鹳雀楼》
const list2 = ref<UTSJSONObject[]>([
{
label: "白日依山尽"
},
{
label: "黄河入海流"
},
{
label: "欲穷千里目"
},
{
label: "更上一层楼"
},
{
label: "一览众山小"
}
]);
const list3 = ref<UTSJSONObject[]>([
{
label: "项目1"
},
{
label: "项目2"
},
{
label: "项目3"
},
{
label: "项目4"
},
{
label: "项目5"
},
{
label: "项目6"
},
{
label: "项目7"
},
{
label: "项目8",
disabled: true
},
{
label: "项目9"
},
{
label: "项目10"
},
{
label: "项目11"
},
{
label: "项目12"
}
]);
const list4 = ref<UTSJSONObject[]>([
{
url: "https://unix.cool-js.com/images/demo/1.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/2.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/3.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/4.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/5.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/6.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/7.jpg"
}
]);
</script>

View File

@@ -0,0 +1,467 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-filter-bar>
<!-- 下拉框 -->
<cl-filter-item
label="综合排序"
type="select"
:value="1"
:options="coreOptions"
:pt="{
className: 'w-[220rpx] !flex-none'
}"
@change="onOptionsChange"
></cl-filter-item>
<!-- 排序 -->
<cl-filter-item
label="销量"
type="sort"
value="desc"
@change="onSortChange"
></cl-filter-item>
<!-- 开关 -->
<cl-filter-item
label="国补"
type="switch"
:value="false"
@change="onSwitchChange"
></cl-filter-item>
<!-- 自定义 -->
<view
class="flex flex-row items-center justify-center flex-1"
@tap="openFilter"
>
<cl-text>筛选</cl-text>
<cl-icon name="filter-line"></cl-icon>
</view>
</cl-filter-bar>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: 'text-sm p-2' }">{{
JSON.stringify(filterForm, null, 4)
}}</cl-text>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: 'text-sm p-2' }">{{
JSON.stringify(searchForm, null, 4)
}}</cl-text>
</demo-item>
</view>
<!-- 自定义筛选 -->
<cl-popup
v-model="filterVisible"
:title="t('筛选')"
direction="right"
size="80%"
:show-header="false"
>
<view class="flex flex-col h-full">
<scroll-view class="flex-1">
<cl-form :pt="{ className: 'p-3' }">
<cl-form-item label="服务/折扣">
<cl-row :gutter="20">
<cl-col :span="8" v-for="(item, index) in disOptions" :key="index">
<cl-checkbox
v-model="searchForm.dis"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.dis.includes(item.value),
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-checkbox>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="价格区间">
<view class="flex flex-row items-center">
<cl-input
v-model="searchForm.minPrice"
type="digit"
placeholder="最低价"
:pt="{
className: 'flex-1',
inner: {
className: 'text-center'
}
}"
></cl-input>
<cl-text
:pt="{
className: 'px-2'
}"
>~</cl-text
>
<cl-input
v-model="searchForm.maxPrice"
type="digit"
placeholder="最高价"
:pt="{
className: 'flex-1',
inner: {
className: 'text-center'
}
}"
></cl-input>
</view>
</cl-form-item>
<cl-form-item label="品牌">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in brandOptions"
:key="index"
>
<cl-checkbox
v-model="searchForm.brand"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.brand.includes(item.value),
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-checkbox>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="内存">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in memoryOptions"
:key="index"
>
<cl-radio
v-model="searchForm.memory"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.memory == item.value,
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-radio>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="颜色">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in colorOptions"
:key="index"
>
<cl-radio
v-model="searchForm.color"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.color == item.value,
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-radio>
</cl-col>
</cl-row>
</cl-form-item>
</cl-form>
</scroll-view>
<view class="flex flex-row p-3">
<cl-button
type="info"
text
border
:pt="{
className: 'flex-1'
}"
@tap="closeFilter"
>{{ t("取消") }}</cl-button
>
<cl-button
:pt="{
className: 'flex-1'
}"
@tap="submit"
>{{ t("确定") }}</cl-button
>
</view>
<cl-safe-area type="bottom"></cl-safe-area>
</view>
</cl-popup>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { reactive, ref } from "vue";
import { useUi, type ClSelectOption } from "@/uni_modules/cool-ui";
import { isDark, parseClass } from "@/cool";
const ui = useUi();
const filterVisible = ref(false);
function openFilter() {
filterVisible.value = true;
}
function closeFilter() {
filterVisible.value = false;
}
function submit() {
closeFilter();
ui.showLoading();
setTimeout(() => {
ui.hideLoading();
}, 1000);
}
const coreOptions = ref<ClSelectOption[]>([
{
label: "综合排序",
value: 1
},
{
label: "价格从高到底",
value: 2
},
{
label: "价格从低到高",
value: 3
}
]);
type Option = {
label: string;
value: string;
};
const disOptions = ref<Option[]>([
{
label: "百亿补贴",
value: "billion_subsidy"
},
{
label: "以旧换新",
value: "trade_in"
},
{
label: "分期免息",
value: "installment"
},
{
label: "包邮",
value: "free_shipping"
},
{
label: "促销",
value: "promotion"
},
{
label: "价保",
value: "price_protection"
},
{
label: "仅看有货",
value: "in_stock"
},
{
label: "货到付款",
value: "cod"
}
]);
const brandOptions = ref<Option[]>([
{
label: "华为",
value: "huawei"
},
{
label: "苹果",
value: "apple"
},
{
label: "小米",
value: "xiaomi"
},
{
label: "三星",
value: "samsung"
},
{
label: "OPPO",
value: "oppo"
},
{
label: "vivo",
value: "vivo"
},
{
label: "荣耀",
value: "honor"
}
]);
const colorOptions = ref<Option[]>([
{
label: "红色",
value: "red"
},
{
label: "蓝色",
value: "blue"
},
{
label: "黑色",
value: "black"
},
{
label: "白色",
value: "white"
},
{
label: "金色",
value: "gold"
},
{
label: "银色",
value: "silver"
},
{
label: "绿色",
value: "green"
},
{
label: "紫色",
value: "purple"
},
{
label: "灰色",
value: "gray"
},
{
label: "粉色",
value: "pink"
}
]);
const memoryOptions = ref<Option[]>([
{
label: "128GB",
value: "128"
},
{
label: "256GB",
value: "256"
},
{
label: "512GB",
value: "512"
},
{
label: "1TB",
value: "1024"
}
]);
type SearchForm = {
dis: string[];
minPrice: string;
maxPrice: string;
brand: string[];
memory: string;
color: string;
};
const searchForm = ref<SearchForm>({
dis: [],
minPrice: "50",
maxPrice: "300",
brand: [],
memory: "",
color: ""
});
type FilterForm = {
core: number;
sort: string;
switch: boolean;
};
const filterForm = reactive<FilterForm>({
core: 0,
sort: "none",
switch: false
});
function onOptionsChange(val: number) {
console.log(val);
filterForm.core = val;
}
function onSortChange(val: string) {
console.log(val);
filterForm.sort = val;
}
function onSwitchChange(val: boolean) {
console.log(val);
filterForm.switch = val;
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<cl-page>
<cl-list-view
ref="listViewRef"
:data="listView"
:virtual="false"
:pt="{
refresher: {
className: 'pt-3'
}
}"
:refresher-enabled="true"
@pull="onPull"
@bottom="loadMore"
>
<template #item="{ value }">
<goods-item :value="value"></goods-item>
</template>
<template #bottom>
<view class="py-3">
<cl-loadmore
v-if="list.length > 0"
:loading="loading"
safe-area-bottom
></cl-loadmore>
</view>
</template>
</cl-list-view>
</cl-page>
</template>
<script lang="ts" setup>
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
import { usePager } from "@/cool";
import GoodsItem from "../components/goods-item.uvue";
import { t } from "@/locale";
const ui = useUi();
const listViewRef = ref<ClListViewComponentPublicInstance | null>(null);
let id = 0;
const { refresh, list, listView, loading, loadMore } = usePager((params, { render }) => {
// 模拟请求
setTimeout(() => {
render({
list: [
{
id: id++,
title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
image: "https://unix.cool-js.com/images/demo/1.jpg"
},
{
id: id++,
title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
image: "https://unix.cool-js.com/images/demo/2.jpg"
},
{
id: id++,
title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
image: "https://unix.cool-js.com/images/demo/3.jpg"
},
{
id: id++,
title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
image: "https://unix.cool-js.com/images/demo/5.jpg"
},
{
id: id++,
title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
image: "https://unix.cool-js.com/images/demo/6.jpg"
},
{
id: id++,
title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
image: "https://unix.cool-js.com/images/demo/7.jpg"
},
{
id: id++,
title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
image: "https://unix.cool-js.com/images/demo/8.jpg"
},
{
id: id++,
title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
image: "https://unix.cool-js.com/images/demo/9.jpg"
},
{
id: id++,
title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
image: "https://unix.cool-js.com/images/demo/10.jpg"
}
],
pagination: {
page: params["page"],
size: params["size"],
total: 100
}
});
ui.hideLoading();
}, 1000);
});
async function onPull() {
await refresh({ page: 1 });
listViewRef.value!.stopRefresh();
}
onReady(() => {
ui.showLoading(t("加载中"));
// 默认请求
refresh({});
});
</script>

View File

@@ -0,0 +1,68 @@
<template>
<cl-page>
<view class="page">
<view class="p-3 pb-0">
<demo-item>
<cl-text
>采用虚拟列表技术实现高性能渲染,支持海量数据无限滚动,当前演示数据规模:{{
data.length
}}条</cl-text
>
</demo-item>
</view>
<view class="list">
<cl-list-view
:data="data"
:pt="{
indexBar: {
className: '!fixed'
},
itemHover: {
className: 'bg-gray-200'
}
}"
>
</cl-list-view>
</view>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { request } from "@/cool";
import DemoItem from "../components/item.uvue";
import { useListView, useUi, type ClListViewItem } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const data = ref<ClListViewItem[]>([]);
onReady(() => {
ui.showLoading();
request({
url: "https://unix.cool-js.com/data/pca_flat.json"
})
.then((res) => {
data.value = useListView(res as UTSJSONObject[]);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
ui.hideLoading();
});
});
</script>
<style lang="scss" scoped>
.page {
height: 100%;
.list {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-list-item :label="t('用户名')">
<cl-text>神仙都没用</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('内容靠左')">
<cl-list-item
:label="t('QQ')"
justify="start"
:pt="{
label: {
className: '!w-10'
}
}"
>
<cl-text>615206459</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('带箭头')">
<cl-list-item label="年龄" arrow>
<cl-text>18</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('带图标')">
<cl-list-item :label="t('余额')" icon="wallet-line">
<cl-text>10,9000</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('带图片')">
<cl-list-item
arrow
:pt="{
image: {
width: 48,
height: 48
}
}"
:label="t('神仙都没用')"
image="https://unix.cool-js.com/images/demo/avatar.jpg"
>
</cl-list-item>
</demo-item>
<demo-item :label="t('折叠')">
<cl-list-item :label="t('点击展开')" collapse arrow>
<template #collapse>
<view class="bg-surface-100 dark:bg-surface-700 p-3 rounded-xl">
<cl-text
>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</cl-text
>
</view>
</template>
</cl-list-item>
</demo-item>
<demo-item :label="t('可滑动')">
<cl-list-item :label="t('左滑编辑')" swipeable>
<template #swipe-right>
<view
class="bg-green-500 w-20 h-full flex flex-row items-center justify-center"
>
<text class="text-white text-md">{{ t("编辑") }}</text>
</view>
</template>
</cl-list-item>
<cl-list-item ref="listItemRef" :label="t('右滑删除')" swipeable>
<template #swipe-left>
<view
class="bg-red-500 w-20 h-full flex flex-row items-center justify-center"
@tap="onDelete"
>
<text class="text-white text-md">{{ t("删除") }}</text>
</view>
</template>
</cl-list-item>
</demo-item>
<demo-item :label="t('禁用')">
<cl-list-item :label="t('账号')" disabled>
<cl-text>1234567890</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('列表')">
<cl-list border>
<cl-list-item :label="t('我的订单')" hoverable> </cl-list-item>
<cl-list-item :label="t('我的收藏')" hoverable> </cl-list-item>
<cl-list-item :label="t('我的钱包')" hoverable> </cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const listItemRef = ref<ClListItemComponentPublicInstance | null>(null);
function onDelete() {
ui.showToast({
message: "删除成功"
});
listItemRef.value!.resetSwipe();
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('横向滚动')">
<cl-marquee
:list="list"
direction="horizontal"
:item-height="200"
:item-width="360"
:pt="{
className: 'h-[200rpx] rounded-xl'
}"
></cl-marquee>
</demo-item>
<demo-item :label="t('纵向滚动')">
<cl-marquee
ref="marqueeRef"
:list="list"
direction="vertical"
:item-height="260"
:duration="isSpeed ? 2000 : 5000"
:pt="{
className: 'h-[500rpx] rounded-xl'
}"
></cl-marquee>
<cl-list
border
:pt="{
className: 'mt-5'
}"
>
<cl-list-item :label="t('快一点')">
<cl-switch v-model="isSpeed"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('暂停')">
<cl-switch v-model="isPause" @change="onPauseChange"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
import { ref } from "vue";
const marqueeRef = ref<ClMarqueeComponentPublicInstance | null>(null);
const list = ref<string[]>([
"https://unix.cool-js.com/images/demo/bg1.png",
"https://unix.cool-js.com/images/demo/bg2.png",
"https://unix.cool-js.com/images/demo/bg3.png"
]);
const isSpeed = ref(false);
const isPause = ref(false);
function onPauseChange(value: boolean) {
if (value) {
marqueeRef.value!.pause();
} else {
marqueeRef.value!.play();
}
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-pagination v-model="page1" :total="24"> </cl-pagination>
</demo-item>
<demo-item :label="t('多页数')">
<cl-pagination v-model="page2" :total="500"> </cl-pagination>
</demo-item>
<demo-item :label="t('自定义样式')">
<cl-pagination
v-model="page3"
:total="100"
:pt="{
item: {
className: '!rounded-none !mx-[2rpx]'
}
}"
>
</cl-pagination>
</demo-item>
<demo-item :label="t('自定义文本')">
<cl-pagination
v-model="page4"
:total="24"
:pt="{
prev: {
className: '!w-auto px-3'
},
next: {
className: '!w-auto px-3'
}
}"
>
<template #prev>
<cl-text
:pt="{
className: 'text-sm'
}"
>{{ t("上一页") }}</cl-text
>
</template>
<template #next>
<cl-text
:pt="{
className: 'text-sm'
}"
>{{ t("下一页") }}</cl-text
>
</template>
</cl-pagination>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
const page1 = ref(1);
const page2 = ref(13);
const page3 = ref(1);
const page4 = ref(1);
</script>

View File

@@ -0,0 +1,110 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-read-more>
<cl-text>
云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。
一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。
名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。
</cl-text>
</cl-read-more>
</demo-item>
<demo-item :label="t('禁用切换按钮')">
<cl-read-more
v-model="visible"
:disabled="disabled"
:expand-text="disabled ? '付费解锁' : '展开'"
:expand-icon="disabled ? 'lock-line' : 'arrow-down-s-line'"
@toggle="toggle"
>
<cl-text>
云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。
一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。
名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。
</cl-text>
</cl-read-more>
</demo-item>
<demo-item :label="t('动态内容')">
<cl-read-more :content="content" :show-toggle="content != ''" ref="readMoreRef">
<view
class="flex flex-row items-center justify-center h-14"
v-if="content == ''"
>
<cl-loading :size="32"></cl-loading>
</view>
</cl-read-more>
</demo-item>
<demo-item :label="t('自定义高度')">
<cl-read-more :height="300">
<cl-text>
云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。
一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。
</cl-text>
<cl-image
:height="300"
width="100%"
:pt="{
className: 'my-3'
}"
src="https://unix.cool-js.com/images/demo/bg1.png"
></cl-image>
<cl-text>
名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。
</cl-text>
</cl-read-more>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const visible = ref(false);
const disabled = ref(true);
const readMoreRef = ref<ClReadMoreComponentPublicInstance | null>(null);
const content = ref("");
function getContent() {
setTimeout(() => {
content.value =
"云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。";
// 使用 slot 插入内容时,如果内容发生变化,需要重新获取高度
// readMoreRef.value!.getContentHeight();
}, 500);
}
function toggle(isExpanded: boolean) {
ui.showConfirm({
title: "提示",
message: "需支付100元才能解锁全部内容是否继续",
callback(action) {
if (action == "confirm") {
ui.showToast({
message: "支付成功"
});
disabled.value = false;
visible.value = true;
}
}
});
}
onReady(() => {
getContent();
});
</script>

View File

@@ -0,0 +1,71 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-timeline>
<cl-timeline-item
icon="account-box-line"
:title="t('开通账号')"
date="2025-01-01"
:content="t('赠送500元')"
>
</cl-timeline-item>
<cl-timeline-item
icon="id-card-line"
:title="t('完成实名认证')"
date="2025-01-02"
:content="t('通过身份证认证')"
>
</cl-timeline-item>
<cl-timeline-item
icon="bank-card-line"
:title="t('绑定银行卡')"
date="2025-01-03"
:content="t('绑定招商银行储蓄卡')"
>
</cl-timeline-item>
<cl-timeline-item
icon="money-cny-box-line"
:title="t('首次充值')"
date="2025-01-04"
:content="t('充值1000元')"
>
</cl-timeline-item>
<cl-timeline-item
icon="checkbox-line"
:title="t('完成首笔交易')"
date="2025-01-05"
:hide-line="true"
>
<view class="flex flex-row mb-3 mt-1">
<cl-image
src="https://unix.cool-js.com/images/demo/bg1.png"
></cl-image>
<view class="flex-1 px-3">
<cl-text>{{ t("优选灵活配置混合A") }}</cl-text>
<cl-text class="mr-5 mt-1 text-sm">{{ t("1000元起") }}</cl-text>
<view class="flex flex-row mt-2 items-center">
<cl-button size="small" type="light">{{
t("立即购买")
}}</cl-button>
</view>
</view>
</view>
</cl-timeline-item>
</cl-timeline>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,391 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('树形结构')">
<cl-tree
v-model="checkedKeys"
ref="treeRef"
:list="list"
:icon="isCustomIcon ? 'add-circle-line' : 'arrow-right-s-fill'"
:expand-icon="isCustomIcon ? 'indeterminate-circle-line' : 'arrow-down-s-fill'"
:checkable="true"
:multiple="true"
:check-strictly="checkStrictly"
></cl-tree>
<cl-list border :pt="{ className: 'mt-5' }">
<cl-list-item :label="t('父子关联')">
<cl-switch v-model="checkStrictly"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('换个图标')">
<cl-switch v-model="isCustomIcon"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('选中值')">
<cl-text>{{ checkedKeys.join("、") }}</cl-text>
</demo-item>
<demo-item :label="t('选中操作')">
<view class="mb-2">
<cl-button @tap="setChecked">{{ t("选中部分节点") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="getChecked">{{ t("获取选中节点") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="getHalfChecked">{{ t("获取半选节点") }}</cl-button>
<cl-text
v-if="halfCheckedKeys.length > 0"
:pt="{
className: 'text-sm p-2'
}"
>{{ halfCheckedKeys.join("、") }}</cl-text
>
</view>
<view class="mb-2">
<cl-button @tap="clearChecked">{{ t("清空选中") }}</cl-button>
</view>
</demo-item>
<demo-item :label="t('展开操作')">
<view class="mb-2">
<cl-button @tap="expand">{{ t("展开部分节点") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="getExpanded">{{ t("获取展开节点") }}</cl-button>
<cl-text
v-if="expandedKeys.length > 0"
:pt="{
className: 'text-sm p-2'
}"
>{{ expandedKeys.join("、") }}</cl-text
>
</view>
<view class="mb-2">
<cl-button @tap="expandAll">{{ t("展开所有") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="collapseAll">{{ t("收起所有") }}</cl-button>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
import { ref } from "vue";
import { useTree, useUi, type ClTreeItem } from "@/uni_modules/cool-ui";
const ui = useUi();
const list = ref<ClTreeItem[]>([]);
function refresh() {
ui.showLoading();
setTimeout(() => {
list.value = useTree([
{
id: "1",
label: "华为",
children: [
{
id: "1-1",
label: "手机",
children: [
{
id: "1-1-1",
label: "Mate系列",
children: [
{
id: "1-1-1-1",
label: "Mate 50"
},
{
id: "1-1-1-2",
disabled: true,
label: "Mate 40"
},
{
id: "1-1-1-3",
label: "Mate 30"
}
]
},
{
id: "1-1-2",
label: "P系列",
children: [
{
id: "1-1-2-1",
disabled: true,
label: "P60"
},
{
id: "1-1-2-2",
label: "P50"
},
{
id: "1-1-2-3",
label: "P40"
}
]
}
]
},
{
id: "1-2",
label: "笔记本",
children: [
{
id: "1-2-1",
label: "MateBook X",
children: [
{
id: "1-2-1-1",
label: "MateBook X Pro"
},
{
id: "1-2-1-2",
label: "MateBook X 2022"
}
]
},
{
id: "1-2-2",
label: "MateBook D",
children: [
{
id: "1-2-2-1",
label: "MateBook D 14"
},
{
id: "1-2-2-2",
label: "MateBook D 15"
}
]
},
{
id: "1-2-3",
label: "MateBook 13"
}
]
}
]
},
{
id: "2",
label: "小米",
isExpand: true,
children: [
{
id: "2-1",
label: "手机",
children: [
{
id: "2-1-1",
label: "小米数字系列"
},
{
id: "2-1-2",
label: "Redmi系列"
}
]
},
{
id: "2-2",
label: "家电",
children: [
{
id: "2-2-1",
label: "电视"
},
{
id: "2-2-2",
label: "空调"
}
]
}
]
},
{
id: "3",
label: "苹果",
children: [
{
id: "3-1",
label: "手机",
children: [
{
id: "3-1-1",
label: "iPhone 14"
},
{
id: "3-1-2",
label: "iPhone 13"
}
]
},
{
id: "3-2",
label: "平板",
children: [
{
id: "3-2-1",
label: "iPad Pro"
},
{
id: "3-2-2",
label: "iPad Air"
}
]
}
]
},
{
id: "4",
label: "OPPO",
children: [
{
id: "4-1",
label: "手机",
children: [
{
id: "4-1-1",
label: "Find系列"
},
{
id: "4-1-2",
label: "Reno系列"
}
]
},
{
id: "4-2",
label: "配件",
children: [
{
id: "4-2-1",
label: "耳机"
},
{
id: "4-2-2",
label: "手环"
}
]
}
]
},
{
id: "5",
label: "vivo",
children: [
{
id: "5-1",
label: "手机",
children: [
{
id: "5-1-1",
label: "X系列"
},
{
id: "5-1-2",
label: "S系列"
}
]
},
{
id: "5-2",
label: "智能设备",
children: [
{
id: "5-2-1",
label: "手表"
},
{
id: "5-2-2",
label: "耳机"
}
]
}
]
}
]);
ui.hideLoading();
}, 500);
}
// 是否严格的遵循父子不互相关联
const checkStrictly = ref(false);
// 是否自定义图标
const isCustomIcon = ref(false);
// 树形组件引用
const treeRef = ref<ClTreeComponentPublicInstance | null>(null);
// 选中节点的keys
const checkedKeys = ref<(string | number)[]>(["1-1-1-1", "2-1-1", "2-1-2"]);
const checkedKeys2 = ref<string | null>("1-1-1");
// 半选节点的keys
const halfCheckedKeys = ref<(string | number)[]>([]);
// 展开节点的keys
const expandedKeys = ref<(string | number)[]>([]);
// 演示方法
function setChecked() {
treeRef.value!.setCheckedKeys(["1-1", "2"]);
}
function getChecked() {
checkedKeys.value = treeRef.value!.getCheckedKeys();
}
function clearChecked() {
treeRef.value!.clearChecked();
checkedKeys.value = [];
halfCheckedKeys.value = [];
}
function getHalfChecked() {
halfCheckedKeys.value = treeRef.value!.getHalfCheckedKeys();
}
function expand() {
treeRef.value!.setExpandedKeys(["4", "5"]);
}
function getExpanded() {
expandedKeys.value = treeRef.value!.getExpandedKeys();
}
function expandAll() {
treeRef.value!.expandAll();
expandedKeys.value = treeRef.value!.getExpandedKeys();
}
function collapseAll() {
treeRef.value!.collapseAll();
expandedKeys.value = [];
}
onReady(() => {
refresh();
});
</script>

View File

@@ -0,0 +1,138 @@
<template>
<cl-page back-top>
<view class="py-2">
<cl-waterfall ref="waterfallRef" :column="2" :gutter="16">
<template #item="{ item, index }">
<view class="bg-white mb-3 rounded-xl dark:!bg-gray-800 relative">
<image
:src="item['image']"
mode="widthFix"
class="w-full rounded-xl"
></image>
<template v-if="item['isAd']">
<cl-tag :pt="{ className: 'absolute left-1 top-1 scale-75' }"
>广告</cl-tag
>
<cl-icon
color="white"
name="close-line"
:pt="{ className: 'absolute right-2 top-2' }"
@tap="del(item['id'] as number)"
></cl-icon>
</template>
<view class="p-3" v-else>
<cl-text>{{ item["title"] }}</cl-text>
<cl-row class="mt-2" :pt="{ className: 'justify-end items-center' }">
<cl-icon name="heart-line"></cl-icon>
<cl-text :pt="{ className: 'text-sm ml-1' }">{{
item["likeCount"]
}}</cl-text>
</cl-row>
</view>
</view>
</template>
</cl-waterfall>
<cl-loadmore :loading="true" safe-area-bottom></cl-loadmore>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { random } from "@/cool";
import { onMounted, ref } from "vue";
const waterfallRef = ref<ClWaterfallComponentPublicInstance | null>(null);
const loading = ref(false);
let id = 0;
function refresh() {
const items = [
{
id: id++,
likeCount: random(100, 1000),
title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
image: "https://unix.cool-js.com/images/demo/1.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
image: "https://unix.cool-js.com/images/demo/2.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
image: "https://unix.cool-js.com/images/demo/3.jpg"
},
{
id: id++,
image: "https://unix.cool-js.com/images/demo/4.jpg",
isAd: true
},
{
id: id++,
likeCount: random(100, 1000),
title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
image: "https://unix.cool-js.com/images/demo/5.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
image: "https://unix.cool-js.com/images/demo/6.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
image: "https://unix.cool-js.com/images/demo/7.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
image: "https://unix.cool-js.com/images/demo/8.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
image: "https://unix.cool-js.com/images/demo/9.jpg"
},
{
id: id++,
likeCount: random(100, 1000),
title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
image: "https://unix.cool-js.com/images/demo/10.jpg"
}
];
waterfallRef.value!.append(items);
}
function del(id: number) {
waterfallRef.value!.remove(id);
}
onReachBottom(() => {
if (loading.value) return;
loading.value = true;
setTimeout(() => {
refresh();
loading.value = false;
}, 1000);
});
onMounted(() => {
refresh();
});
</script>

View File

@@ -0,0 +1,206 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-button @tap="openActionSheet">{{ t("打开") }}</cl-button>
</demo-item>
<demo-item :label="t('带图标')">
<cl-button @tap="openActionSheet2">{{ t("打开") }}</cl-button>
</demo-item>
<demo-item :label="t('带标题、描述')">
<cl-button @tap="openActionSheet3">{{ t("打开") }}</cl-button>
</demo-item>
<demo-item :label="t('无法点击遮罩关闭')">
<cl-button @tap="openActionSheet4">{{ t("打开") }}</cl-button>
</demo-item>
<demo-item :label="t('不需要取消按钮')">
<cl-button @tap="openActionSheet5">{{ t("打开") }}</cl-button>
</demo-item>
<demo-item :label="t('插槽用法')">
<cl-button @tap="openActionSheet6">{{ t("打开") }}</cl-button>
</demo-item>
</view>
<cl-action-sheet ref="actionSheetRef"> </cl-action-sheet>
<cl-action-sheet ref="actionSheetRef2"> </cl-action-sheet>
<cl-action-sheet ref="actionSheetRef3"> </cl-action-sheet>
<cl-action-sheet ref="actionSheetRef4"> </cl-action-sheet>
<cl-action-sheet ref="actionSheetRef5"> </cl-action-sheet>
<cl-action-sheet
ref="actionSheetRef6"
:pt="{
list: {
className: 'flex-row mx-[-10rpx]'
},
item: {
className: 'flex-1 mx-[10rpx] !rounded-xl'
}
}"
>
<template #prepend>
<view class="px-3 mb-3">
<cl-text>开通会员享受更多特权和服务,包括无广告体验、专属客服等</cl-text>
</view>
</template>
<template #append>
<view class="pb-5 pt-2 px-3 flex flex-row items-center">
<cl-checkbox v-model="agree">请阅读并同意</cl-checkbox>
<cl-text color="primary">《会员服务协议》</cl-text>
</view>
</template>
<template #item="{ item }">
<view class="flex flex-col justify-center items-center">
<cl-icon :name="item.icon" :size="46"></cl-icon>
<cl-text :pt="{ className: 'text-sm mt-1' }">{{ item.label }}</cl-text>
</view>
</template>
</cl-action-sheet>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { useUi, type ClActionSheetOptions } from "@/uni_modules/cool-ui";
const ui = useUi();
const actionSheetRef = ref<ClActionSheetComponentPublicInstance | null>(null);
function openActionSheet() {
actionSheetRef.value!.open({
list: [
{
label: t("反馈")
}
]
} as ClActionSheetOptions);
}
const actionSheetRef2 = ref<ClActionSheetComponentPublicInstance | null>(null);
function openActionSheet2() {
actionSheetRef.value!.open({
list: [
{
label: t("反馈"),
icon: "error-warning-line"
}
]
} as ClActionSheetOptions);
}
const actionSheetRef3 = ref<ClActionSheetComponentPublicInstance | null>(null);
function openActionSheet3() {
actionSheetRef.value!.open({
title: t("提示"),
description: t("删除好友会同时删除所有聊天记录"),
list: [
{
label: t("删除好友"),
color: "error",
callback() {
ui.showConfirm({
title: t("提示"),
message: t("确定要删除好友吗?"),
callback(action) {
if (action == "confirm") {
ui.showToast({
message: t("删除成功")
});
}
actionSheetRef.value!.close();
}
});
}
}
]
} as ClActionSheetOptions);
}
const actionSheetRef4 = ref<ClActionSheetComponentPublicInstance | null>(null);
function openActionSheet4() {
actionSheetRef.value!.open({
maskClosable: false,
description: t("无法点击遮罩关闭"),
list: []
} as ClActionSheetOptions);
}
const actionSheetRef5 = ref<ClActionSheetComponentPublicInstance | null>(null);
function openActionSheet5() {
actionSheetRef.value!.open({
showCancel: false,
list: [
{
label: t("点我关闭"),
callback() {
ui.showConfirm({
title: t("提示"),
message: t("确定要关闭吗?"),
callback(action) {
if (action == "confirm") {
actionSheetRef.value!.close();
}
}
});
}
}
]
} as ClActionSheetOptions);
}
const agree = ref(false);
const actionSheetRef6 = ref<ClActionSheetComponentPublicInstance | null>(null);
function openActionSheet6() {
function done() {
if (!agree.value) {
ui.showToast({
message: "请阅读并同意《会员服务协议》"
});
return;
}
ui.showToast({
message: t("支付成功")
});
agree.value = false;
actionSheetRef6.value!.close();
}
actionSheetRef6.value!.open({
showCancel: false,
list: [
{
label: t("支付宝"),
icon: "alipay-line",
callback() {
done();
}
},
{
label: t("微信"),
icon: "wechat-line",
callback() {
done();
}
}
]
} as ClActionSheetOptions);
}
</script>

View File

@@ -0,0 +1,94 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义')">
<cl-button @tap="openPopup">{{ t("打开弹窗") }}</cl-button>
</demo-item>
<demo-item :label="t('隐藏取消按钮')">
<cl-button @tap="openPopup2">{{ t("打开弹窗") }}</cl-button>
</demo-item>
<demo-item :label="t('自定义文本')">
<cl-button @tap="openPopup3">{{ t("打开弹窗") }}</cl-button>
</demo-item>
<demo-item :label="t('关闭前钩子')">
<cl-button @tap="openPopup4">{{ t("打开弹窗") }}</cl-button>
</demo-item>
<demo-item :label="t('显示时长')">
<cl-button @tap="openPopup5">{{ t("打开弹窗") }}</cl-button>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
function openPopup() {
ui.showConfirm({
title: t("提示"),
message: t("确定要删除吗?"),
callback(action) {
if (action == "confirm") {
ui.showToast({
message: t("确定")
});
} else {
ui.showToast({
message: t("取消")
});
}
}
});
}
function openPopup2() {
ui.showConfirm({
title: t("提示"),
message: t("确定要删除吗?"),
showCancel: false
});
}
function openPopup3() {
ui.showConfirm({
title: t("提示"),
message: t("确定要删除吗?"),
cancelText: t("关闭"),
confirmText: t("下一步")
});
}
function openPopup4() {
ui.showConfirm({
title: t("提示"),
message: t("确定要删除吗?"),
beforeClose: (action, { close, showLoading, hideLoading }) => {
if (action == "confirm") {
showLoading();
setTimeout(() => {
close();
}, 1000);
} else {
close();
}
}
});
}
function openPopup5() {
ui.showConfirm({
title: t("提示"),
message: t("确定要删除吗3秒后自动关闭"),
duration: 3000
});
}
</script>

View File

@@ -0,0 +1,100 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义')">
<cl-button @tap="openPopup">{{ t("打开弹窗") }}</cl-button>
<cl-list border class="mt-3">
<view class="w-full p-2">
<cl-tabs
v-model="direction"
:list="directionList"
show-slider
:height="66"
></cl-tabs>
</view>
<cl-list-item :label="t('设置宽度 80%')">
<cl-switch v-model="isWidth"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('无头')">
<cl-switch v-model="unShowHeader"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义样式')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
<cl-popup
v-model="visible"
:title="t('标题')"
:direction="direction"
:size="size"
:show-header="!unShowHeader"
:pt="{
className: parseClass([[isCustom, '!p-3']]),
inner: {
className: parseClass([[isCustom, '!rounded-2xl']])
}
}"
>
<view
class="p-3"
:class="{
'pt-0': !unShowHeader
}"
>
<cl-image
src="https://unix.cool-js.com/images/demo/bg1.png"
class="mb-3"
height="auto"
width="100%"
mode="widthFix"
></cl-image>
<cl-text
>春江花月夜, 花草复青青。 江水流不尽, 月光照无情。 夜来风雨急, 愁思满心头。
何时再相见, 共赏月明楼。
</cl-text>
</view>
</cl-popup>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { computed, ref } from "vue";
import type { ClPopupDirection, ClTabsItem } from "@/uni_modules/cool-ui";
import { parseClass } from "@/cool";
const visible = ref(false);
const isWidth = ref(true);
const unShowHeader = ref(false);
const isCustom = ref(false);
const direction = ref<ClPopupDirection>("bottom");
const directionList = ref<ClTabsItem[]>([
{ label: t("底部"), value: "bottom" },
{ label: t("顶部"), value: "top" },
{ label: t("左侧"), value: "left" },
{ label: t("右侧"), value: "right" },
{ label: t("中间"), value: "center" }
]);
const size = computed(() => {
if (direction.value == "left" || direction.value == "right" || direction.value == "center") {
return isWidth.value ? "80%" : "";
}
return "";
});
function openPopup() {
visible.value = true;
}
</script>

View File

@@ -0,0 +1,93 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-button @tap="open()">{{ t("打开") }}</cl-button>
</demo-item>
<demo-item :label="t('不同位置')">
<view class="flex flex-row overflow-visible">
<cl-button type="light" @tap="open('top')">{{ t("顶部") }}</cl-button>
<cl-button type="light" @tap="open('center')">{{ t("中间") }}</cl-button>
<cl-button type="light" @tap="open('bottom')">{{ t("底部") }}</cl-button>
</view>
</demo-item>
<demo-item :label="t('不同类型')">
<view class="flex flex-row flex-wrap mb-2 overflow-visible">
<cl-button type="light" @tap="openType('success')">{{ t("成功") }}</cl-button>
<cl-button type="light" @tap="openType('error')">{{ t("失败") }}</cl-button>
<cl-button type="light" @tap="openType('warn')">{{ t("警告") }}</cl-button>
<cl-button type="light" @tap="openType('question')">{{ t("问题") }}</cl-button>
<cl-button type="light" @tap="openType('disabled')">{{ t("禁用") }}</cl-button>
</view>
<view class="flex flex-row flex-wrap overflow-visible">
<cl-button type="light" @tap="openType('stop')">{{ t("停止") }}</cl-button>
</view>
</demo-item>
<demo-item :label="t('自定义图标')">
<view class="flex flex-row overflow-visible">
<cl-button
type="light"
icon="star-line"
@tap="openIcon('star-line')"
></cl-button>
<cl-button
type="light"
icon="mail-line"
@tap="openIcon('mail-line')"
></cl-button>
<cl-button
type="light"
icon="file-line"
@tap="openIcon('file-line')"
></cl-button>
</view>
</demo-item>
<demo-item :label="t('只存在一个')">
<cl-button @tap="openClear()">{{ t("打开") }}</cl-button>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { useUi, type ClToastType } from "@/uni_modules/cool-ui";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
const ui = useUi();
function open(position: "top" | "center" | "bottom" = "center") {
ui.showToast({
message: t("不同位置提示"),
position: position
});
}
function openType(type: ClToastType) {
ui.showToast({
message: t("不同类型提示"),
type
});
}
function openIcon(icon: string) {
ui.showToast({
message: t("带图标提示"),
icon
});
}
function openClear() {
ui.showToast({
message: t("移除其他已存在的提示"),
clear: true
});
}
</script>

View File

@@ -0,0 +1,206 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('选择器')">
<cl-calendar-select v-model="date"></cl-calendar-select>
</demo-item>
<demo-item :label="t('多选')">
<cl-calendar-select v-model:date="dateArr" mode="multiple"></cl-calendar-select>
</demo-item>
<demo-item :label="t('范围选')">
<cl-calendar-select v-model:date="dateRange" mode="range"></cl-calendar-select>
</demo-item>
<demo-item :label="t('开始 / 结束')">
<cl-calendar-select
v-model:date="dateRange3"
mode="range"
:start="startDate"
:end="endDate"
></cl-calendar-select>
</demo-item>
<demo-item :label="t('禁用部分日期')">
<cl-calendar-select v-model="date" :date-config="dateConfig"></cl-calendar-select>
</demo-item>
<!-- <demo-item :label="t('日历长列表')">
<cl-button>{{ t("打开日历长列表") }}</cl-button>
</demo-item> -->
<demo-item :label="t('日历面板')">
<cl-calendar
v-model:date="dateRange2"
mode="range"
:month="10"
:show-header="isShowHeader"
:show-weeks="isShowWeeks"
:show-other-month="isShowOtherMonth"
:date-config="dateConfig2"
@change="onChange"
></cl-calendar>
<cl-list
border
:pt="{
className: 'mt-5'
}"
>
<cl-list-item :label="t('自定义文案和颜色')">
<cl-switch v-model="isCustomDateConfig"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示头')">
<cl-switch v-model="isShowHeader"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示星期')">
<cl-switch v-model="isShowWeeks"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示其他月份')">
<cl-switch v-model="isShowOtherMonth"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { dayUts, first, last } from "@/cool";
import type { ClCalendarDateConfig } from "@/uni_modules/cool-ui";
const date = ref<string | null>(dayUts().format("YYYY-MM-DD"));
const dateArr = ref<string[]>([
dayUts().format("YYYY-MM-DD"),
dayUts().add(1, "day").format("YYYY-MM-DD")
]);
const dateRange = ref<string[]>([
dayUts().format("YYYY-MM-DD"),
dayUts().add(10, "day").format("YYYY-MM-DD")
]);
const dateConfig = ref<ClCalendarDateConfig[]>([
{
date: dayUts().add(1, "day").format("YYYY-MM-DD"),
disabled: true
},
{
date: dayUts().add(2, "day").format("YYYY-MM-DD"),
disabled: true
},
{
date: dayUts().add(3, "day").format("YYYY-MM-DD"),
disabled: true
}
]);
const isShowHeader = ref(true);
const isShowWeeks = ref(true);
const isShowOtherMonth = ref(true);
const isCustomDateConfig = ref(true);
const dateRange2 = ref<string[]>([]);
const dateConfig2 = computed(() => {
const dates = (
isCustomDateConfig.value
? [
{
date: "2025-10-01",
topText: "国庆节",
bottomText: "¥958",
color: "red"
},
{
date: "2025-10-02",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-03",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-04",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-05",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-06",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-07",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-08",
topText: "休",
bottomText: "¥613",
color: "red"
}
]
: []
) as ClCalendarDateConfig[];
const startDate = first(dateRange2.value);
const endDate = last(dateRange2.value);
if (startDate != null) {
const item = dates.find((e) => e.date == startDate);
if (item == null) {
dates.push({
date: startDate,
bottomText: "入住"
} as ClCalendarDateConfig);
} else {
item.bottomText = "入住";
}
}
if (endDate != null && dateRange2.value.length > 1) {
const item = dates.find((e) => e.date == endDate);
if (item == null) {
dates.push({
date: endDate,
bottomText: "离店"
} as ClCalendarDateConfig);
} else {
item.bottomText = "离店";
}
}
return dates;
});
const dateRange3 = ref<string[]>([]);
const startDate = dayUts().format("YYYY-MM-DD");
const endDate = dayUts().add(10, "day").format("YYYY-MM-DD");
function onChange(date: string[]) {
console.log("日期变化:", date);
}
</script>

View File

@@ -0,0 +1,509 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-cascader v-model="form.cascader1" :options="options"></cl-cascader>
</demo-item>
<demo-item :label="t('带索引、地区选择')">
<cl-cascader v-model="form.cascader2" :options="options2"></cl-cascader>
</demo-item>
<demo-item :label="t('自定义')">
<cl-cascader
v-model="form.cascader3"
:options="options"
:disabled="isDisabled"
:text-separator="isSeparator ? ' / ' : ' - '"
:height="isHeight ? 500 : 800"
></cl-cascader>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('换个分隔符')">
<cl-switch v-model="isSeparator"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('列表高度小一点')">
<cl-switch v-model="isHeight"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import DemoItem from "../components/item.uvue";
import { useCascader, type ClCascaderOption } from "@/uni_modules/cool-ui";
import { t } from "@/locale";
import pca from "@/data/pca.json";
type Form = {
cascader1: string[];
cascader2: string[];
cascader3: string[];
};
const form = reactive<Form>({
cascader1: [],
cascader2: [],
cascader3: []
});
const isDisabled = ref(false);
const isSeparator = ref(false);
const isHeight = ref(false);
const options = ref<ClCascaderOption[]>([
{
label: "电子产品",
value: "1",
children: [
{
label: "手机",
value: "1-1",
children: [
{
label: "苹果",
value: "1-1-1",
children: [
{
label: "iPhone 15 Pro Max",
value: "1-1-1-1"
},
{
label: "iPhone 15 Pro",
value: "1-1-1-2"
},
{
label: "iPhone 15",
value: "1-1-1-3"
},
{
label: "iPhone 14 Pro Max",
value: "1-1-1-4"
},
{
label: "iPhone 14",
value: "1-1-1-5"
}
]
},
{
label: "华为",
value: "1-1-2",
children: [
{
label: "Mate 60 Pro+",
value: "1-1-2-1"
},
{
label: "Mate 60 Pro",
value: "1-1-2-2"
},
{
label: "Mate 60",
value: "1-1-2-3"
},
{
label: "P60 Pro",
value: "1-1-2-4"
},
{
label: "P60",
value: "1-1-2-5"
}
]
},
{
label: "小米",
value: "1-1-3",
children: [
{
label: "小米14 Pro",
value: "1-1-3-1"
},
{
label: "小米14",
value: "1-1-3-2"
},
{
label: "Redmi K70 Pro",
value: "1-1-3-3"
},
{
label: "Redmi K70",
value: "1-1-3-4"
}
]
}
]
},
{
label: "电脑",
value: "1-2",
children: [
{
label: "笔记本",
value: "1-2-1",
children: [
{
label: "MacBook Pro 16",
value: "1-2-1-1"
},
{
label: "MacBook Pro 14",
value: "1-2-1-2"
},
{
label: "MacBook Air 15",
value: "1-2-1-3"
},
{
label: "ThinkPad X1",
value: "1-2-1-4"
},
{
label: "ROG 魔霸新锐",
value: "1-2-1-5"
},
{
label: "拯救者 Y9000P",
value: "1-2-1-6"
}
]
},
{
label: "台式机",
value: "1-2-2",
children: [
{
label: "iMac 24寸",
value: "1-2-2-1"
},
{
label: "Mac Studio",
value: "1-2-2-2"
},
{
label: "Mac Pro",
value: "1-2-2-3"
},
{
label: "外星人",
value: "1-2-2-4"
},
{
label: "惠普暗影精灵",
value: "1-2-2-5"
}
]
}
]
},
{
label: "平板",
value: "1-3",
children: [
{
label: "iPad",
value: "1-3-1",
children: [
{
label: "iPad Pro 12.9",
value: "1-3-1-1"
},
{
label: "iPad Pro 11",
value: "1-3-1-2"
},
{
label: "iPad Air",
value: "1-3-1-3"
},
{
label: "iPad mini",
value: "1-3-1-4"
}
]
},
{
label: "安卓平板",
value: "1-3-2",
children: [
{
label: "小米平板6 Pro",
value: "1-3-2-1"
},
{
label: "华为MatePad Pro",
value: "1-3-2-2"
},
{
label: "三星Galaxy Tab S9",
value: "1-3-2-3"
}
]
}
]
}
]
},
{
label: "服装",
value: "2",
children: [
{
label: "男装",
value: "2-1",
children: [
{
label: "上衣",
value: "2-1-1",
children: [
{
label: "短袖T恤",
value: "2-1-1-1"
},
{
label: "长袖T恤",
value: "2-1-1-2"
},
{
label: "衬衫",
value: "2-1-1-3"
},
{
label: "卫衣",
value: "2-1-1-4"
},
{
label: "夹克",
value: "2-1-1-5"
},
{
label: "毛衣",
value: "2-1-1-6"
}
]
},
{
label: "裤装",
value: "2-1-2",
children: [
{
label: "牛仔裤",
value: "2-1-2-1"
},
{
label: "休闲裤",
value: "2-1-2-2"
},
{
label: "运动裤",
value: "2-1-2-3"
},
{
label: "西裤",
value: "2-1-2-4"
},
{
label: "短裤",
value: "2-1-2-5"
}
]
},
{
label: "外套",
value: "2-1-3",
children: [
{
label: "羽绒服",
value: "2-1-3-1"
},
{
label: "大衣",
value: "2-1-3-2"
},
{
label: "夹克",
value: "2-1-3-3"
},
{
label: "西装",
value: "2-1-3-4"
}
]
}
]
},
{
label: "女装",
value: "2-2",
children: [
{
label: "裙装",
value: "2-2-1",
children: [
{
label: "连衣裙",
value: "2-2-1-1"
},
{
label: "半身裙",
value: "2-2-1-2"
},
{
label: "A字裙",
value: "2-2-1-3"
},
{
label: "包臀裙",
value: "2-2-1-4"
},
{
label: "百褶裙",
value: "2-2-1-5"
}
]
},
{
label: "上装",
value: "2-2-2",
children: [
{
label: "衬衫",
value: "2-2-2-1"
},
{
label: "T恤",
value: "2-2-2-2"
},
{
label: "毛衣",
value: "2-2-2-3"
},
{
label: "卫衣",
value: "2-2-2-4"
},
{
label: "雪纺衫",
value: "2-2-2-5"
}
]
},
{
label: "外套",
value: "2-2-3",
children: [
{
label: "风衣",
value: "2-2-3-1"
},
{
label: "羽绒服",
value: "2-2-3-2"
},
{
label: "大衣",
value: "2-2-3-3"
},
{
label: "西装",
value: "2-2-3-4"
},
{
label: "皮衣",
value: "2-2-3-5"
}
]
}
]
}
]
},
{
label: "食品",
value: "3",
children: [
{
label: "水果",
value: "3-1",
children: [
{
label: "苹果",
value: "3-1-1"
},
{
label: "香蕉",
value: "3-1-2"
},
{
label: "橘子",
value: "3-1-3"
}
]
},
{
label: "蔬菜",
value: "3-2",
children: [
{
label: "西红柿",
value: "3-2-1"
},
{
label: "黄瓜",
value: "3-2-2"
},
{
label: "胡萝卜",
value: "3-2-3"
}
]
}
]
},
{
label: "饮料",
value: "4",
children: [
{
label: "果汁",
value: "4-1",
children: [
{
label: "苹果汁",
value: "4-1-1"
},
{
label: "橙汁",
value: "4-1-2"
},
{
label: "葡萄汁",
value: "4-1-3"
},
{
label: "西瓜汁",
value: "4-1-4"
}
]
}
]
}
]);
const options2 = useCascader(pca);
</script>

View File

@@ -0,0 +1,163 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<view class="flex flex-row flex-wrap">
<cl-checkbox
v-model="checked2"
v-for="(item, index) in options"
:key="index"
:value="item.value"
:pt="{
className: 'mr-5'
}"
>
{{ item.label }}
</cl-checkbox>
</view>
</demo-item>
<demo-item :label="t('单个 true / false')">
<view class="flex flex-row items-center">
<cl-checkbox v-model="checked4">同意并阅读</cl-checkbox>
<cl-text color="primary">《用户协议》</cl-text>
<view class="flex-1"></view>
<cl-switch v-model="checked4"></cl-switch>
</view>
</demo-item>
<demo-item :label="t('纵向排列')">
<cl-checkbox
v-model="checked"
v-for="(item, index) in options"
:key="index"
:value="item.value"
:pt="{
className: parseClass([
'py-2',
[
isVerticalCustom,
'justify-between border border-solid border-surface-200 rounded-lg p-2 !my-1'
]
])
}"
>
{{ item.label }}
</cl-checkbox>
<cl-list
border
:pt="{
className: 'mt-2'
}"
>
<cl-list-item :label="t('换个样式')">
<cl-switch v-model="isVerticalCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('自定义')">
<view class="mb-3 flex flex-row flex-wrap">
<cl-checkbox
v-model="checked3"
v-for="(item, index) in options"
:key="index"
:value="item.value"
:disabled="isDisabled"
:show-icon="!isHideIcon"
:active-icon="isIcon ? 'heart-fill' : 'checkbox-line'"
:inactive-icon="isIcon ? 'heart-line' : 'checkbox-blank-line'"
:pt="{
className: parseClass([
'mr-5',
[isCustom, 'bg-surface-100 py-2 px-3 rounded-lg !mr-2 !mb-2'],
{
'!bg-surface-700': isDark && isCustom
}
]),
icon: {
className: parseClass([
[
isCustom && checked3.includes(item.value as string),
'text-red-500'
]
])
},
label: {
className: parseClass([
[
isCustom && checked3.includes(item.value as string),
'text-red-500'
]
])
}
}"
>
{{ item.label }}
</cl-checkbox>
</view>
<cl-list border>
<cl-list-item :label="t('换个图标')">
<cl-switch v-model="isIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('不显示图标')">
<cl-switch v-model="isHideIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('其他样式')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { useUi, type ClCheckboxOption } from "@/uni_modules/cool-ui";
import { isDark, parseClass } from "@/cool";
import { t } from "@/locale";
const ui = useUi();
const isIcon = ref(false);
const isCustom = ref(false);
const isDisabled = ref(false);
const isHideIcon = ref(false);
const isVerticalCustom = ref(false);
const checked = ref<string[]>([]);
const checked2 = ref<string[]>(["2"]);
const checked3 = ref<string[]>(["2", "3"]);
const checked4 = ref(false);
const options = ref<ClCheckboxOption[]>([
{
label: "Vue",
value: "1"
},
{
label: "React",
value: "2"
},
{
label: "Angular",
value: "3"
},
{
label: "Svelte",
value: "4"
}
]);
</script>

View File

@@ -0,0 +1,381 @@
<template>
<cl-page>
<view class="p-3">
<demo-item>
<cl-form
:pt="{
className: 'p-2 pb-0'
}"
v-model="formData"
ref="formRef"
:rules="rules"
:disabled="saving"
label-position="top"
>
<cl-form-item prop="avatarUrl">
<cl-upload v-model="formData.avatarUrl" test></cl-upload>
</cl-form-item>
<cl-form-item :label="t('用户名')" prop="nickName" required>
<cl-input
v-model="formData.nickName"
:placeholder="t('请输入用户名')"
clearable
></cl-input>
</cl-form-item>
<cl-form-item :label="t('邮箱')" prop="email">
<cl-input
v-model="formData.email"
:placeholder="t('请输入邮箱地址')"
></cl-input>
</cl-form-item>
<cl-form-item :label="t('动态验证')" required prop="contacts">
<view
class="contacts border border-solid border-surface-200 rounded-xl p-3 dark:!border-surface-700"
>
<cl-form-item
v-for="(item, index) in formData.contacts"
:key="index"
:label="t('联系人') + ` - ${index + 1}`"
:prop="`contacts[${index}].phone`"
:rules="
[
{
required: true,
message: t('手机号不能为空')
}
] as ClFormRule[]
"
required
>
<view class="flex flex-row items-center">
<cl-input
:pt="{
className: 'flex-1 mr-2'
}"
v-model="item.phone"
:placeholder="t('请输入手机号')"
></cl-input>
<cl-button
type="light"
icon="subtract-line"
@tap="removeContact(index)"
></cl-button>
</view>
</cl-form-item>
<cl-button icon="add-line" @tap="addContact">{{
t("添加联系人")
}}</cl-button>
</view>
</cl-form-item>
<cl-form-item :label="t('身高')" prop="height" required>
<cl-slider v-model="formData.height" :max="220" show-value>
<template #value="{ value }">
<cl-text
:pt="{
className: 'text-center w-[120rpx]'
}"
>{{ value }} cm</cl-text
>
</template>
</cl-slider>
</cl-form-item>
<cl-form-item :label="t('体重')" prop="weight" required>
<cl-slider v-model="formData.weight" :max="150" show-value>
<template #value="{ value }">
<cl-text
:pt="{
className: 'text-center w-[120rpx]'
}"
>{{ value }} kg</cl-text
>
</template>
</cl-slider>
</cl-form-item>
<cl-form-item :label="t('标签')" prop="tags" required>
<view class="flex flex-row flex-wrap">
<cl-checkbox
v-model="formData.tags"
v-for="(item, index) in tagsOptions"
:key="index"
:value="index"
:pt="{
className: 'mr-5 mt-2'
}"
>{{ item.label }}</cl-checkbox
>
</view>
</cl-form-item>
<cl-form-item :label="t('性别')" prop="gender" required>
<cl-select v-model="formData.gender" :options="genderOptions"></cl-select>
</cl-form-item>
<cl-form-item :label="t('所在地区')" prop="pca" required>
<cl-cascader v-model="formData.pca" :options="pcaOptions"></cl-cascader>
</cl-form-item>
<cl-form-item :label="t('出生年月')" prop="birthday" required>
<cl-select-date v-model="formData.birthday" type="date"></cl-select-date>
</cl-form-item>
<cl-form-item :label="t('个人简介')" prop="description">
<cl-textarea
v-model="formData.description"
:placeholder="t('请输入个人简介')"
:maxlength="200"
></cl-textarea>
</cl-form-item>
<cl-form-item :label="t('公开状态')">
<cl-switch v-model="formData.isPublic"></cl-switch>
</cl-form-item>
</cl-form>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: 'text-sm p-2' }">{{
JSON.stringify(formData, null, 4)
}}</cl-text>
</demo-item>
</view>
<cl-footer>
<view class="flex flex-row">
<cl-button type="info" :pt="{ className: 'flex-1' }" @click="reset">{{
t("重置")
}}</cl-button>
<cl-button
type="primary"
:loading="saving"
:pt="{ className: 'flex-1' }"
@click="submit"
>{{ t("提交") }}</cl-button
>
</view>
</cl-footer>
</cl-page>
</template>
<script setup lang="ts">
import { ref, type Ref } from "vue";
import DemoItem from "../components/item.uvue";
import {
useCascader,
useForm,
useUi,
type ClFormRule,
type ClSelectOption
} from "@/uni_modules/cool-ui";
import pca from "@/data/pca.json";
import { t } from "@/locale";
import { dayUts } from "@/cool";
const ui = useUi();
const { formRef, validate, clearValidate } = useForm();
// 性别选项
const genderOptions = [
{
label: t("未知"),
value: 0
},
{
label: t("男"),
value: 1
},
{
label: t("女"),
value: 2
}
] as ClSelectOption[];
// 标签选项
const tagsOptions = [
{
label: t("篮球"),
value: 1
},
{
label: t("足球"),
value: 2
},
{
label: t("羽毛球"),
value: 3
},
{
label: t("乒乓球"),
value: 4
},
{
label: t("游泳"),
value: 5
}
] as ClSelectOption[];
// 地区选项
const pcaOptions = useCascader(pca);
type Contact = {
phone: string;
};
// 自定义表单数据类型
type FormData = {
avatarUrl: string;
nickName: string;
email: string;
height: number;
weight: number;
gender: number;
description: string;
pca: string[];
tags: number[];
birthday: string;
isPublic: boolean;
contacts: Contact[];
};
// 表单数据
const formData = ref<FormData>({
avatarUrl: "",
nickName: "神仙都没用",
email: "",
height: 180,
weight: 70,
gender: 0,
description: "",
pca: [],
tags: [1, 2],
birthday: "",
isPublic: false,
contacts: []
}) as Ref<FormData>;
// 表单验证规则
const rules = new Map<string, ClFormRule[]>([
[
"nickName",
[
{ required: true, message: t("用户名不能为空") },
{ min: 3, max: 20, message: t("用户名长度在3-20个字符之间") }
]
],
[
"email",
[
{ required: true, message: t("邮箱不能为空") },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: t("邮箱格式不正确") }
]
],
[
"height",
[
{ required: true, message: t("身高不能为空") },
{ min: 160, max: 190, message: t("身高在160-190cm之间") }
]
],
[
"weight",
[
{ required: true, message: t("体重不能为空") },
{ min: 40, max: 100, message: t("体重在40-100kg之间") }
]
],
[
"tags",
[
{ required: true, message: t("标签不能为空") },
{ min: 1, max: 2, message: t("标签最多选择2个") }
]
],
["gender", [{ required: true, message: t("性别不能为空") }]],
["pca", [{ required: true, message: t("所在地区不能为空") }]],
[
"birthday",
[
{ required: true, message: t("出生年月不能为空") },
{
validator(value) {
if (dayUts(value).isAfter(dayUts("2010-01-01"))) {
return t("出生年月不大于2010-01-01");
}
return true;
}
}
]
],
[
"contacts",
[
{
required: true,
message: t("联系人不能为空")
}
]
]
]);
// 是否保存中
const saving = ref(false);
// 重置表单数据
function reset() {
formData.value.avatarUrl = "";
formData.value.nickName = "";
formData.value.email = "";
formData.value.height = 180;
formData.value.weight = 70;
formData.value.gender = 0;
formData.value.description = "";
formData.value.pca = [];
formData.value.tags = [];
formData.value.birthday = "";
formData.value.isPublic = false;
formData.value.contacts = [];
clearValidate();
}
// 提交表单
function submit() {
validate((valid, errors) => {
if (valid) {
saving.value = true;
setTimeout(() => {
ui.showToast({
message: t("提交成功"),
icon: "check-line"
});
saving.value = false;
reset();
}, 2000);
} else {
ui.showToast({
message: errors[0].message
});
}
});
}
function addContact() {
formData.value.contacts.push({
phone: ""
});
}
function removeContact(index: number) {
formData.value.contacts.splice(index, 1);
}
</script>

View File

@@ -0,0 +1,93 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-input-number></cl-input-number>
</demo-item>
<demo-item :label="t('自定义')">
<view class="mb-5 flex flex-row justify-center">
<cl-input-number
v-model="num"
:step="isStep ? 10 : 1"
:min="isMin ? 10 : 0"
:max="isMax ? 50 : 100"
:input-type="isDigit ? 'digit' : 'number'"
:inputable="isInput"
:disabled="isDisabled"
:size="isSize ? 60 : 50"
:pt="{
op: {
className: parseClass({
'!rounded-full': isCustom
})
},
value: {
className: parseClass({
'!rounded-full': isCustom
})
}
}"
@change="onChange"
></cl-input-number>
</view>
<cl-list border>
<cl-list-item :label="t('步进为10')">
<cl-switch v-model="isStep"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('最小为10')">
<cl-switch v-model="isMin"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('最大为50')">
<cl-switch v-model="isMax"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('可以小数')">
<cl-switch v-model="isDigit"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('可以输入')">
<cl-switch v-model="isInput"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('大一点')">
<cl-switch v-model="isSize"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义样式')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { parseClass } from "@/cool";
import { t } from "@/locale";
const num = ref(0);
const isStep = ref(false);
const isMin = ref(false);
const isMax = ref(false);
const isDigit = ref(false);
const isInput = ref(true);
const isDisabled = ref(false);
const isSize = ref(false);
const isCustom = ref(false);
function onChange(value: number) {
console.log(value);
}
</script>

View File

@@ -0,0 +1,73 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-input-otp @done="toDone"></cl-input-otp>
</demo-item>
<demo-item :label="t('自动聚焦')">
<cl-input-otp autofocus @done="toDone"></cl-input-otp>
</demo-item>
<demo-item :label="t('自定义')">
<cl-input-otp
:length="isLength ? 6 : 4"
:disabled="isDisabled"
:pt="{
item: {
className: parseClass({
'!bg-sky-100': isCustom,
'!border-white': isCustom
})
},
value: {
className: parseClass({
'!text-sky-700': isCustom
})
},
cursor: {
className: parseClass({
'!bg-sky-700': isCustom
})
}
}"
@done="toDone"
></cl-input-otp>
<cl-list border :pt="{ className: 'mt-5' }">
<cl-list-item :label="t('长度为6')">
<cl-switch v-model="isLength"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('其他样式')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { useUi } from "@/uni_modules/cool-ui";
import { parseClass } from "@/cool";
import { t } from "@/locale";
const ui = useUi();
const isLength = ref(true);
const isDisabled = ref(false);
const isCustom = ref(false);
function toDone() {
ui.showToast({
message: "Done"
});
}
</script>

View File

@@ -0,0 +1,136 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-input></cl-input>
</demo-item>
<demo-item :label="t('数字输入')">
<cl-input type="number"></cl-input>
</demo-item>
<demo-item :label="t('密码输入')">
<cl-input password></cl-input>
</demo-item>
<demo-item :label="t('可清除')">
<cl-input clearable :pt="{ className: 'mb-2' }"></cl-input>
<demo-tips>设置 hold-keyboard 属性后,清除内容时输入框将保持聚焦状态</demo-tips>
<cl-input clearable hold-keyboard></cl-input>
</demo-item>
<demo-item :label="t('左右插槽')">
<cl-input
:pt="{
className: '!pr-1 mb-2'
}"
>
<template #append>
<cl-button
type="primary"
size="small"
icon="send-plane-fill"
:pt="{
className: 'ml-2'
}"
@tap="toAlert"
></cl-button>
</template>
</cl-input>
<cl-input
:pt="{
className: '!pl-1'
}"
>
<template #prepend>
<cl-button
type="primary"
size="small"
icon="search-line"
:pt="{
className: 'mr-2'
}"
@tap="toAlert"
></cl-button>
</template>
</cl-input>
</demo-item>
<demo-item :label="t('自定义')">
<cl-input
v-model="content"
:border="isBorder"
:suffix-icon="isRightIcon ? 'text' : ''"
:prefix-icon="isLeftIcon ? 'search-line' : ''"
:disabled="isDisabled"
:pt="{
className: parseClass({
'!bg-sky-100': isColor,
'!border-sky-700': isColor
}),
inner: {
className: parseClass({
'!text-sky-700': isColor
})
},
prefixIcon: {
className: parseClass({
'!text-sky-700': isColor
})
}
}"
></cl-input>
<cl-list border :pt="{ className: 'mt-5' }">
<cl-list-item :label="t('边框')">
<cl-switch v-model="isBorder"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('左图标')">
<cl-switch v-model="isLeftIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('右图标')">
<cl-switch v-model="isRightIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('其他颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import DemoTips from "../components/tips.uvue";
import { parseClass } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
import { t } from "@/locale";
const ui = useUi();
const content = ref("Cool Unix");
const isBorder = ref(true);
const isLeftIcon = ref(true);
const isRightIcon = ref(false);
const isDisabled = ref(false);
const isColor = ref(false);
function toAlert() {
ui.showToast({
message: "Hello"
});
}
</script>

View File

@@ -0,0 +1,111 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('数字键盘')">
<view class="mb-3 overflow-visible">
<cl-input type="number" v-model="content"></cl-input>
</view>
<cl-button @tap="openKeyboardNumber">{{ t("打开键盘") }}</cl-button>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('是否显示输入值')">
<cl-switch v-model="isShowValue"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('输入即绑定')">
<cl-switch v-model="isInputImmediate"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('身份证键盘')">
<cl-switch v-model="isIdcard"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('密码键盘')">
<cl-button @tap="openKeyboardPassword">{{ t("打开键盘") }}</cl-button>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('是否加密')">
<cl-switch v-model="isEncrypt"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('车牌号键盘')">
<view class="flex mb-3 justify-center flex-row overflow-visible">
<cl-input-otp
input-type="text"
:length="8"
:pt="{
className: 'w-full',
list: {
className: 'justify-between'
},
item: {
className: '!h-9 !w-9'
},
cursor: {
className: '!h-3'
}
}"
v-model="carNumber"
></cl-input-otp>
</view>
<cl-button @tap="openKeyboardCar">{{ t("打开键盘") }}</cl-button>
</demo-item>
</view>
<cl-keyboard-number
v-model="content"
ref="keyboardNumberRef"
:show-value="isShowValue"
:input-immediate="isInputImmediate"
:type="isIdcard ? 'idcard' : 'number'"
></cl-keyboard-number>
<cl-keyboard-car v-model="carNumber" ref="keyboardCarRef"></cl-keyboard-car>
<cl-keyboard-password ref="keyboardPasswordRef" :encrypt="isEncrypt"></cl-keyboard-password>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
const keyboardNumberRef = ref<ClKeyboardNumberComponentPublicInstance | null>(null);
const isShowValue = ref(true);
const isInputImmediate = ref(false);
const isIdcard = ref(false);
const content = ref("");
function openKeyboardNumber() {
keyboardNumberRef.value!.open();
}
const keyboardPasswordRef = ref<ClKeyboardPasswordComponentPublicInstance | null>(null);
const isEncrypt = ref(false);
function openKeyboardPassword() {
keyboardPasswordRef.value!.open();
}
const keyboardCarRef = ref<ClKeyboardCarComponentPublicInstance | null>(null);
const carNumber = ref("");
function openKeyboardCar() {
keyboardCarRef.value!.open();
}
</script>

View File

@@ -0,0 +1,146 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<view class="flex flex-row flex-wrap">
<cl-radio
v-model="checked2"
v-for="(item, index) in options"
:key="index"
:value="item.value"
:pt="{
className: 'mr-5'
}"
>
{{ item.label }}
</cl-radio>
</view>
</demo-item>
<demo-item :label="t('纵向排列')">
<cl-radio
v-model="checked"
v-for="(item, index) in options"
:key="index"
:value="item.value"
:pt="{
className: parseClass([
'my-2',
[
isVerticalCustom,
'justify-between border border-solid border-surface-200 rounded-lg p-2 !my-1'
]
])
}"
>
{{ item.label }}
</cl-radio>
<cl-list
border
:pt="{
className: 'mt-2'
}"
>
<cl-list-item :label="t('换个样式')">
<cl-switch v-model="isVerticalCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('自定义')">
<view class="mb-3 flex flex-row flex-wrap">
<cl-radio
v-model="checked3"
v-for="(item, index) in options"
:key="index"
:value="item.value"
:disabled="isDisabled"
:show-icon="!isHideIcon"
:active-icon="isIcon ? 'heart-fill' : 'checkbox-circle-line'"
:inactive-icon="isIcon ? 'heart-line' : 'checkbox-blank-circle-line'"
:pt="{
className: parseClass([
'mr-5',
[isCustom, 'bg-surface-100 py-2 px-3 rounded-lg !mr-2 !mb-2'],
{
'!bg-surface-700': isDark && isCustom
}
]),
icon: {
className: parseClass([
[isCustom && item.value == checked3, 'text-red-500']
])
},
label: {
className: parseClass([
[isCustom && item.value == checked3, 'text-red-500']
])
}
}"
>
{{ item.label }}
</cl-radio>
</view>
<cl-list border>
<cl-list-item :label="t('换个图标')">
<cl-switch v-model="isIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('不显示图标')">
<cl-switch v-model="isHideIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('其他样式')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { parseClass } from "@/cool";
import { useUi, type ClRadioOption } from "@/uni_modules/cool-ui";
import { isDark } from "@/cool";
import { t } from "@/locale";
const ui = useUi();
const isIcon = ref(false);
const isCustom = ref(false);
const isDisabled = ref(false);
const isHideIcon = ref(false);
const isVerticalCustom = ref(false);
const checked = ref("1");
const checked2 = ref("2");
const checked3 = ref("3");
const options = ref<ClRadioOption[]>([
{
label: "Vue",
value: "1"
},
{
label: "React",
value: "2"
},
{
label: "Angular",
value: "3"
},
{
label: "Svelte",
value: "4"
}
]);
</script>

View File

@@ -0,0 +1,62 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-rate v-model="num"></cl-rate>
</demo-item>
<demo-item :label="t('自定义')">
<cl-rate
v-model="num2"
:disabled="isDisabled"
:show-score="isShowScore"
:allow-half="isAllowHalf"
:size="isSize ? 50 : 40"
:void-icon="isIcon ? 'heart-fill' : 'star-fill'"
:icon="isIcon ? 'heart-fill' : 'star-fill'"
></cl-rate>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('只读')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示分数')">
<cl-switch v-model="isShowScore"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('允许半星')">
<cl-switch v-model="isAllowHalf"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('换个图标')">
<cl-switch v-model="isIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('大一点')">
<cl-switch v-model="isSize"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
const num = ref(1);
const num2 = ref(3.5);
const isDisabled = ref(false);
const isShowScore = ref(true);
const isAllowHalf = ref(true);
const isSize = ref(false);
const isIcon = ref(false);
</script>

View File

@@ -0,0 +1,286 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-select-date
v-model="form.date1"
type="date"
@change="onDateChange"
></cl-select-date>
</demo-item>
<demo-item :label="t('固定开始、结束日期')">
<cl-select-date
v-model="form.date3"
start="2025-06-01"
end="2027-08-01"
type="date"
></cl-select-date>
</demo-item>
<demo-item :label="t('自定义触发器')">
<view class="flex flex-row">
<cl-button @tap="openSelect4">{{ t("打开选择器") }}</cl-button>
</view>
<cl-select-date
ref="selectRef4"
v-model="form.date4"
type="date"
:show-trigger="false"
></cl-select-date>
</demo-item>
<demo-item :label="t('范围选择')">
<cl-text :pt="{ className: 'mb-3' }">{{ form.date5 }}</cl-text>
<cl-select-date v-model:values="form.date5" type="date" rangeable></cl-select-date>
</demo-item>
<demo-item :label="t('自定义快捷选项')">
<cl-select-date
v-model:values="form.date6"
type="date"
rangeable
:shortcuts="shortcuts"
></cl-select-date>
</demo-item>
<demo-item :label="t('自定义')">
<cl-select-date
v-model="form.date7"
:type="type"
:label-format="labelFormat"
:disabled="isDisabled"
></cl-select-date>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item label="YYYY">
<cl-switch v-model="isYear"></cl-switch>
</cl-list-item>
<cl-list-item label="YYYY-MM">
<cl-switch v-model="isMonth"></cl-switch>
</cl-list-item>
<cl-list-item
label="YYYY-MM-DD"
:pt="{
label: {
className: 'flex-[2]'
}
}"
>
<cl-switch v-model="isDate"></cl-switch>
</cl-list-item>
<cl-list-item
label="YYYY-MM-DD HH"
:pt="{
label: {
className: 'flex-[2]'
}
}"
>
<cl-switch v-model="isHour"></cl-switch>
</cl-list-item>
<cl-list-item
label="YYYY-MM-DD HH:mm"
:pt="{
label: {
className: 'flex-[2]'
}
}"
>
<cl-switch v-model="isMinute"></cl-switch>
</cl-list-item>
<cl-list-item
label="YYYY-MM-DD HH:mm:ss"
:pt="{
label: {
className: 'flex-[2]'
}
}"
>
<cl-switch v-model="isSecond"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('标签格式化')">
<cl-switch v-model="isFormat"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
import { useUi, type ClSelectDateShortcut } from "@/uni_modules/cool-ui";
import { dayUts } from "@/cool";
const ui = useUi();
// 测试日期选择变化事件
function onDateChange(value: string) {
console.log("日期选择变化:", value);
ui.showToast({
message: `选择了日期: ${value}`,
type: "success"
});
}
type Form = {
date1: string;
date2: string;
date3: string;
date4: string;
date5: string[];
date6: string[];
date7: string;
};
const form = reactive<Form>({
date1: "2023-06-24",
date2: "",
date3: "",
date4: "",
date5: [],
date6: [],
date7: ""
});
const isDisabled = ref(false);
const isYear = ref(false);
const isMonth = ref(false);
const isDate = ref(true);
const isHour = ref(false);
const isMinute = ref(false);
const isSecond = ref(false);
const isFormat = ref(false);
const type = computed(() => {
if (isSecond.value) {
return "second";
}
if (isMinute.value) {
return "minute";
}
if (isHour.value) {
return "hour";
}
if (isDate.value) {
return "date";
}
if (isMonth.value) {
return "month";
}
if (isYear.value) {
return "year";
}
return "second";
});
const labelFormat = computed(() => {
if (isFormat.value) {
if (isSecond.value) {
return "YYYY年MM月DD日 HH时mm分ss秒";
}
if (isMinute.value) {
return "YYYY年MM月DD日 HH时mm分";
}
if (isHour.value) {
return "YYYY年MM月DD日 HH时";
}
if (isDate.value) {
return "YYYY年MM月DD日";
}
if (isMonth.value) {
return "YYYY年MM月";
}
if (isYear.value) {
return "YYYY年";
}
} else {
if (isSecond.value) {
return "YYYY-MM-DD HH:mm:ss";
}
if (isMinute.value) {
return "YYYY-MM-DD HH:mm";
}
if (isHour.value) {
return "YYYY-MM-DD HH";
}
if (isDate.value) {
return "YYYY-MM-DD";
}
if (isMonth.value) {
return "YYYY-MM";
}
if (isYear.value) {
return "YYYY";
}
}
return "YYYY-MM-DD HH:mm:ss";
});
const selectRef4 = ref<ClSelectDateComponentPublicInstance | null>(null);
function openSelect4() {
selectRef4.value!.open((value) => {
ui.showToast({
message: `你选择了:${value}`
});
});
}
const shortcuts = ref<ClSelectDateShortcut[]>([
{
label: "昨日",
value: [dayUts().subtract(1, "day").format("YYYY-MM-DD"), dayUts().format("YYYY-MM-DD")]
},
{
label: "本周",
value: [
dayUts().startOf("week").add(1, "day").format("YYYY-MM-DD"),
dayUts().endOf("week").add(1, "day").format("YYYY-MM-DD")
]
},
{
label: "本月",
value: [
dayUts().startOf("month").format("YYYY-MM-DD"),
dayUts().endOf("month").format("YYYY-MM-DD")
]
}
]);
</script>

View File

@@ -0,0 +1,123 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-select-time v-model="form.time1"></cl-select-time>
</demo-item>
<demo-item :label="t('自定义触发器')">
<view class="flex flex-row">
<cl-button @tap="openSelect2">{{ t("打开选择器") }} </cl-button>
</view>
<cl-select-time
ref="selectRef2"
v-model="form.time2"
:show-trigger="false"
></cl-select-time>
</demo-item>
<demo-item :label="t('自定义')">
<cl-select-time
v-model="form.time3"
:disabled="isDisabled"
:label-format="labelFormat"
:type="type"
></cl-select-time>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('时')">
<cl-switch v-model="isHour"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('时:分')">
<cl-switch v-model="isMinute"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('时:分:秒')">
<cl-switch v-model="isSecond"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('标签格式化')">
<cl-switch v-model="isFormat"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import DemoItem from "../components/item.uvue";
import { useUi } from "@/uni_modules/cool-ui";
import { t } from "@/locale";
const ui = useUi();
type Form = {
time1: string;
time2: string;
time3: string;
};
const form = reactive<Form>({
time1: "",
time2: "",
time3: ""
});
const isDisabled = ref(false);
const isFormat = ref(false);
const isHour = ref(false);
const isMinute = ref(false);
const isSecond = ref(true);
const selectRef2 = ref<ClSelectTimeComponentPublicInstance | null>(null);
function openSelect2() {
selectRef2.value!.open((value) => {
ui.showToast({
message: `你选择了:${value}`
});
});
}
const type = computed(() => {
if (isHour.value) {
return "hour";
}
if (isMinute.value) {
return "minute";
}
return "second";
});
const labelFormat = computed(() => {
if (isFormat.value) {
switch (type.value) {
case "hour":
return "{H}时";
case "minute":
return "{H}时{m}分";
case "second":
return "{H}时{m}分{s}秒";
default:
return null;
}
} else {
return null;
}
});
</script>

View File

@@ -0,0 +1,388 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-select v-model="form.selected" :options="options"></cl-select>
</demo-item>
<demo-item :label="t('自定义触发器')">
<view class="flex flex-row">
<cl-button @tap="openSelect2">{{ t("打开选择器") }}</cl-button>
</view>
<cl-select
ref="selectRef2"
v-model="form.selected2"
:options="options2"
:show-trigger="false"
></cl-select>
</demo-item>
<demo-item :label="t('多列')">
<demo-tips>
{{ t("通过 children 配置多级数据,并使用 column-count 参数指定显示的列数") }}
</demo-tips>
<cl-select
v-model="form.selected3"
:options="options3"
:column-count="3"
></cl-select>
</demo-item>
<demo-item :label="t('弹窗中使用')">
<cl-button @tap="visible3 = true">{{ t("打开") }}</cl-button>
<cl-popup v-model="visible3" direction="center" size="80%" :title="t('选择地区')">
<view class="p-3 pt-0">
<demo-tips>
H5 和 APP 端通过 teleport 实现弹窗内的选择器使用,小程序端则通过
root-portal 实现。
</demo-tips>
<cl-select
v-model="form.selected3"
:options="options3"
:column-count="3"
></cl-select>
</view>
</cl-popup>
</demo-item>
<demo-item :label="t('自定义')">
<cl-text
:pt="{
className: 'mb-3 text-sm p-3 bg-surface-100 dark:!bg-surface-700 rounded-lg'
}"
v-if="form.selected4 != null && isShowValue"
>
{{ t("绑定值") }}{{ form.selected4 }}
</cl-text>
<cl-select
v-model="form.selected4"
:options="options"
:disabled="isDisabled"
:show-cancel="isShowCancel"
:confirm-text="isButtonText ? t('下一步') : t('确定')"
:cancel-text="isButtonText ? t('关闭') : t('取消')"
></cl-select>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('显示取消按钮')">
<cl-switch v-model="isShowCancel"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('修改按钮文案')">
<cl-switch v-model="isButtonText"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示绑定值')">
<cl-switch v-model="isShowValue"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('空数据')">
<cl-select v-model="form.selected" :options="options4"></cl-select>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import DemoItem from "../components/item.uvue";
import DemoTips from "../components/tips.uvue";
import { useUi, type ClSelectOption } from "@/uni_modules/cool-ui";
import { t } from "@/locale";
const ui = useUi();
const selectRef2 = ref<ClSelectComponentPublicInstance | null>(null);
const isShowCancel = ref(true);
const isButtonText = ref(false);
const isDisabled = ref(false);
const isShowValue = ref(false);
type Form = {
selected: number | null; // 参数 clear 需要指定类型带有 null
selected2: string;
selected3: number[];
selected4: number | null;
};
const form = reactive<Form>({
selected: null,
selected2: "2",
selected3: [],
selected4: 3
});
const options = ref<ClSelectOption[]>([
{
label: "HTML",
value: 1
},
{
label: "CSS",
value: 2
},
{
label: "JavaScript",
value: 3
},
{
label: "Node",
value: 4
},
{
label: "Vite",
value: 5
},
{
label: "Webpack",
value: 6
}
]);
const options2 = ref<ClSelectOption[]>([
{
label: "Vue",
value: "1"
},
{
label: "React",
value: "2"
},
{
label: "Angular",
value: "3"
},
{
label: "Svelte",
value: "4"
}
]);
const options3 = ref<ClSelectOption[]>([
{
label: "福建省",
value: 1,
children: [
{
label: "福州市",
value: 1,
children: [
{
label: "鼓楼区",
value: 1
},
{
label: "台江区",
value: 2
},
{
label: "仓山区",
value: 3
},
{
label: "马尾区",
value: 4
}
]
},
{
label: "厦门市",
value: 2,
children: [
{
label: "思明区",
value: 1
},
{
label: "湖里区",
value: 2
},
{
label: "集美区",
value: 3
},
{
label: "海沧区",
value: 4
}
]
},
{
label: "泉州市",
value: 3,
children: [
{
label: "鲤城区",
value: 1
},
{
label: "丰泽区",
value: 2
},
{
label: "洛江区",
value: 3
},
{
label: "泉港区",
value: 4
}
]
}
]
},
{
label: "浙江省",
value: 2,
children: [
{
label: "杭州市",
value: 1,
children: [
{
label: "上城区",
value: 1
},
{
label: "下城区",
value: 2
},
{
label: "江干区",
value: 3
},
{
label: "拱墅区",
value: 4
}
]
},
{
label: "宁波市",
value: 2,
children: [
{
label: "海曙区",
value: 1
},
{
label: "江北区",
value: 2
},
{
label: "北仑区",
value: 3
}
]
}
]
},
{
label: "湖南省",
value: 3,
children: [
{
label: "长沙市",
value: 1,
children: [
{
label: "芙蓉区",
value: 1
},
{
label: "天心区",
value: 2
},
{
label: "岳麓区",
value: 3
}
]
},
{
label: "株洲市",
value: 2,
children: [
{
label: "荷塘区",
value: 1
},
{
label: "芦淞区",
value: 2
}
]
}
]
},
{
label: "江西省",
value: 4,
children: [
{
label: "南昌市",
value: 1,
children: [
{
label: "东湖区",
value: 1
},
{
label: "西湖区",
value: 2
},
{
label: "青云谱区",
value: 3
}
]
},
{
label: "九江市",
value: 2,
children: [
{
label: "浔阳区",
value: 1
},
{
label: "濂溪区",
value: 2
}
]
}
]
}
]);
const options4 = ref<ClSelectOption[]>([]);
function openSelect2() {
selectRef2.value!.open((value) => {
const d = options2.value.find((item) => item.value == value);
ui.showToast({
message: `你选择了 ${value} : ${d?.label}`
});
});
}
const visible3 = ref(false);
</script>

View File

@@ -0,0 +1,86 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-slider v-model="num"></cl-slider>
</demo-item>
<demo-item :label="t('范围选择')">
<cl-text
:pt="{
className: 'mb-3'
}"
>{{ num2[0] }} {{ num2[1] }}</cl-text
>
<cl-slider v-model:values="num2" range></cl-slider>
</demo-item>
<demo-item :label="t('自定义')">
<cl-slider
v-model="num3"
:disabled="isDisabled"
:show-value="isShowValue"
:block-size="isSize ? 50 : 40"
:track-height="isSize ? 12 : 8"
:step="isStep ? 10 : 1"
:max="isMax ? 50 : 100"
:pt="{
thumb: {
className: isColor ? '!bg-red-500' : ''
},
progress: {
className: isColor ? '!bg-red-200' : ''
}
}"
></cl-slider>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('显示值')">
<cl-switch v-model="isShowValue"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('步长10')">
<cl-switch v-model="isStep"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('滑块大点')">
<cl-switch v-model="isSize"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('换个颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('最大50')">
<cl-switch v-model="isMax"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
const num = ref(60);
const num2 = ref<number[]>([10, 20]);
const num3 = ref(35);
const isDisabled = ref(false);
const isShowValue = ref(true);
const isStep = ref(false);
const isSize = ref(false);
const isMax = ref(false);
const isColor = ref(false);
</script>

View File

@@ -0,0 +1,92 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-switch v-model="checked"></cl-switch>
</demo-item>
<demo-item :label="t('自定义颜色')">
<view class="flex flex-row">
<cl-switch
v-model="checked"
:pt="{
className: 'mr-5',
track: {
className: '!bg-red-100'
},
thumb: {
className: '!bg-red-500'
}
}"
></cl-switch>
<cl-switch
v-model="checked"
:pt="{
track: {
className: '!bg-purple-100'
},
thumb: {
className: '!bg-purple-500'
}
}"
></cl-switch>
</view>
</demo-item>
<demo-item :label="t('自定义')">
<cl-switch
v-model="checked"
:loading="isLoading"
:disabled="isDisabled"
:height="isSize ? 60 : 48"
:width="isSize ? 100 : 80"
:pt="{
track: {
className: parseClass([[isCustom, '!rounded-md']])
},
thumb: {
className: parseClass([[isCustom, '!rounded-md']])
}
}"
></cl-switch>
<cl-list
border
:pt="{
className: 'mt-3'
}"
>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('加载中')">
<cl-switch v-model="isLoading"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('大一点')">
<cl-switch v-model="isSize"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('正方形')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { parseClass } from "@/cool";
import { t } from "@/locale";
const checked = ref(false);
const isDisabled = ref(false);
const isLoading = ref(false);
const isSize = ref(false);
const isCustom = ref(false);
</script>

View File

@@ -0,0 +1,67 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-textarea></cl-textarea>
</demo-item>
<demo-item :label="t('自定义')">
<cl-textarea
v-model="content"
:border="isBorder"
:disabled="isDisabled"
:show-word-limit="isShowCount"
:auto-height="isAutoHeight"
:pt="{
className: parseClass({
'!bg-sky-100': isColor,
'!border-sky-700': isColor
}),
inner: {
className: parseClass({
'text-sky-700': isColor
})
}
}"
></cl-textarea>
<cl-list border :pt="{ className: 'mt-5' }">
<cl-list-item :label="t('边框')">
<cl-switch v-model="isBorder"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示字数')">
<cl-switch v-model="isShowCount"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自动增高')">
<cl-switch v-model="isAutoHeight"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('其他颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { parseClass } from "@/cool";
import { t } from "@/locale";
const content = ref("Cool Unix");
const isBorder = ref(true);
const isShowCount = ref(true);
const isDisabled = ref(false);
const isColor = ref(false);
const isAutoHeight = ref(false);
</script>

View File

@@ -0,0 +1,50 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-upload v-model="form.upload1" test></cl-upload>
</demo-item>
<demo-item :label="t('禁用')">
<cl-upload v-model="form.upload1" disabled test></cl-upload>
</demo-item>
<demo-item :label="t('自定义图标、文字、大小')">
<cl-upload
v-model="form.upload1"
icon="id-card-line"
:text="t('上传证件照')"
:width="300"
:height="200"
test
></cl-upload>
</demo-item>
<demo-item :label="t('多选')">
<cl-upload multiple v-model="form.upload2" test></cl-upload>
</demo-item>
<demo-item :label="t('限制 3 个')">
<cl-upload multiple :limit="3" v-model="form.upload3" test></cl-upload>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { reactive } from "vue";
type Form = {
upload1: string;
upload2: string[];
upload3: string[];
};
const form = reactive<Form>({
upload1: "",
upload2: [],
upload3: []
});
</script>

View File

@@ -0,0 +1,45 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-button @click="toggle">{{ visible ? t("点击收起") : t("点击展开") }}</cl-button>
<cl-collapse v-model="visible">
<cl-text
>云想衣裳花想容,春风拂槛露华浓,若非群玉山头见,会向瑶台月下逢。</cl-text
>
</cl-collapse>
</demo-item>
<demo-item :label="t('ref 方式调用')">
<cl-button @click="refToggle">{{ t("点击展开") }}</cl-button>
<cl-collapse ref="collapseRef">
<view class="bg-surface-100 p-3 rounded-xl dark:!bg-surface-700">
<cl-text
>云想衣裳花想容,春风拂槛露华浓,若非群玉山头见,会向瑶台月下逢。</cl-text
>
</view>
</cl-collapse>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
const visible = ref(false);
function toggle() {
visible.value = !visible.value;
}
const collapseRef = ref<ClCollapseComponentPublicInstance | null>(null);
function refToggle() {
collapseRef.value!.toggle();
}
</script>

View File

@@ -0,0 +1,183 @@
<template>
<cl-page>
<view
class="p-3"
:class="{
'is-dark': isDark
}"
>
<demo-item :label="t('基础用法')">
<cl-row :gutter="12">
<cl-col :span="8">
<view class="item">
<cl-text :pt="{ className: 'text' }">1</cl-text>
</view>
</cl-col>
<cl-col :span="8">
<view class="item">
<cl-text :pt="{ className: 'text' }">2</cl-text>
</view>
</cl-col>
<cl-col :span="8">
<view class="item">
<cl-text :pt="{ className: 'text' }">3</cl-text>
</view>
</cl-col>
</cl-row>
<cl-row :gutter="12" :pt="{ className: 'mt-3' }">
<cl-col :span="12">
<view class="item">
<cl-text :pt="{ className: 'text' }">1</cl-text>
</view>
</cl-col>
<cl-col :span="12">
<view class="item">
<cl-text :pt="{ className: 'text' }">2</cl-text>
</view>
</cl-col>
</cl-row>
<cl-row :gutter="12" :pt="{ className: 'mt-3' }">
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">1</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">2</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">3</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">4</cl-text>
</view>
</cl-col>
</cl-row>
</demo-item>
<demo-item :label="t('左间隔')">
<cl-row :gutter="12">
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">1</cl-text>
</view>
</cl-col>
<cl-col :span="6" :offset="6">
<view class="item active">
<cl-text :pt="{ className: 'text' }">2</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">3</cl-text>
</view>
</cl-col>
</cl-row>
</demo-item>
<demo-item :label="t('右移动')">
<cl-row :gutter="12">
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">1</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">2</cl-text>
</view>
</cl-col>
<cl-col :span="6" :push="6">
<view class="item active">
<cl-text :pt="{ className: 'text' }">3</cl-text>
</view>
</cl-col>
</cl-row>
</demo-item>
<demo-item :label="t('左移动')">
<cl-row :gutter="12">
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">1</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">2</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="item">
<cl-text :pt="{ className: 'text' }">3</cl-text>
</view>
</cl-col>
<cl-col :span="6" :pull="6">
<view class="item active">
<cl-text :pt="{ className: 'text' }">4</cl-text>
</view>
</cl-col>
</cl-row>
</demo-item>
<demo-item :label="t('多个数据')">
<cl-row :gutter="12">
<cl-col :span="4" v-for="item in 20" :key="item">
<view class="item mb-2">
<cl-text :pt="{ className: 'text' }">{{ item }}</cl-text>
</view>
</cl-col>
</cl-row>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { isDark } from "@/cool";
</script>
<style lang="scss" scoped>
.item {
@apply h-8 bg-surface-100 rounded-md flex flex-row items-center justify-center;
.text {
@apply text-sm text-surface-700;
}
&.active {
@apply bg-primary-500;
.text {
@apply text-white;
}
}
}
.is-dark {
.item {
@apply bg-surface-700;
.text {
@apply text-surface-100;
}
&.active {
@apply bg-primary-500;
.text {
@apply text-white;
}
}
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<cl-page>
<view class="p-3">
<demo-item>
<view class="flex flex-row items-center">
<cl-icon name="notification-4-fill" class="mr-2"></cl-icon>
<cl-text>{{ t("禁用状态,无法拖拽") }}</cl-text>
</view>
</demo-item>
<demo-item>
<view class="flex flex-row items-center">
<cl-icon name="heart-fill" class="mr-2"></cl-icon>
<cl-text>{{ t("不吸附边缘,任意位置可拖拽") }}</cl-text>
</view>
</demo-item>
<cl-float-view :left="200" :bottom="50" :no-snapping="true">
<view
class="w-[40px] h-[40px] bg-primary-500 flex flex-row items-center justify-center"
@tap="toAlert"
>
<cl-icon name="heart-fill" color="white"></cl-icon>
</view>
</cl-float-view>
<cl-float-view disabled :left="20" :bottom="400" :gap="10">
<view
class="w-[40px] h-[40px] bg-surface-400 rounded-full flex flex-row items-center justify-center"
>
<cl-icon name="notification-4-fill" color="white"></cl-icon>
</view>
</cl-float-view>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import DemoItem from "../components/item.uvue";
const ui = useUi();
function toAlert() {
ui.showToast({
message: t("这是一个提示")
});
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<cl-page>
<view class="p-3">
<demo-item>
<cl-text>解决底部按钮隐藏时页面底部仍有空白间距</cl-text>
<cl-text>解决固定定位时内容占位缺失</cl-text>
</demo-item>
<cl-list>
<cl-list-item :label="`${i}`" v-for="i in 50" :key="i"> </cl-list-item>
</cl-list>
</view>
<cl-footer :vt="cache.key">
<template v-if="status == 0">
<view class="flex flex-row">
<cl-button :pt="{ className: 'flex-1' }" text border size="large" @tap="cancel">
{{ t("取消订单") }}
</cl-button>
<cl-button :pt="{ className: 'flex-1' }" type="primary" size="large" @tap="buy">
{{ t("立即购买") }}
</cl-button>
</view>
</template>
<cl-button type="error" size="large" @tap="confirm" v-if="status == 1">
{{ t("确认收货") }}
</cl-button>
<cl-button type="success" size="large" @tap="comment" v-if="status == 2">
{{ t("评价") }}
</cl-button>
</cl-footer>
</cl-page>
</template>
<script lang="ts" setup>
import { useCache } from "@/cool";
import { t } from "@/locale";
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
const status = ref(0);
const { cache } = useCache(() => [status.value]);
function cancel() {
status.value = 3;
}
function buy() {
status.value = 1;
}
function confirm() {
status.value = 2;
}
function comment() {
status.value = 3;
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<cl-page back-top>
<cl-sticky>
<view class="bg-primary-500 p-3 h-[40px] flex flex-row items-center">
<cl-text color="white">Header - 1</cl-text>
</view>
</cl-sticky>
<view class="p-3">
<cl-list>
<cl-list-item :label="`${i}`" v-for="i in 50" :key="i"> </cl-list-item>
</cl-list>
</view>
<cl-sticky :offset-top="40">
<view class="bg-red-500 p-3 h-[40px] flex flex-row items-center">
<cl-text color="white">Header - 2</cl-text>
</view>
</cl-sticky>
<view class="p-3">
<cl-list>
<cl-list-item :label="`${i}`" v-for="i in 50" :key="i"> </cl-list-item>
</cl-list>
</view>
<cl-sticky :offset-top="80">
<view class="bg-purple-500 p-3 h-[40px] flex flex-row items-center">
<cl-text color="white">Header - 3</cl-text>
</view>
</cl-sticky>
<view class="p-3">
<cl-list>
<cl-list-item :label="`${i}`" v-for="i in 50" :key="i"> </cl-list-item>
</cl-list>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,170 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-tabs v-model="form.val1" :list="list"></cl-tabs>
</demo-item>
<demo-item :label="t('显示滑块')">
<cl-tabs
v-model="form.val2"
:list="list"
show-slider
:pt="{ className: isPad ? '!p-2' : '' }"
></cl-tabs>
<cl-list border :pt="{ className: 'mt-3' }">
<cl-list-item :label="t('添加间距')">
<cl-switch v-model="isPad"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('横向填充')">
<demo-tips>{{ t("适用于标签数量不多的情况") }}</demo-tips>
<cl-tabs v-model="form.val3" :list="list2" fill></cl-tabs>
</demo-item>
<demo-item :label="t('居中')">
<view class="flex flex-row justify-center">
<cl-tabs v-model="form.val4" :list="list2"></cl-tabs>
</view>
</demo-item>
<demo-item :label="t('单个禁用')">
<cl-tabs v-model="form.val5" :list="list3"></cl-tabs>
</demo-item>
<demo-item :label="t('自定义')">
<cl-tabs
v-model="form.val6"
:list="list"
:show-line="isShowLine"
:disabled="isDisabled"
:color="isColor ? 'red' : ''"
:un-color="isColor ? '#ccc' : ''"
></cl-tabs>
<cl-list border :pt="{ className: 'mt-3' }">
<cl-list-item :label="t('显示下划线')">
<cl-switch v-model="isShowLine"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import DemoTips from "../components/tips.uvue";
import { reactive, ref } from "vue";
import type { ClTabsItem } from "@/uni_modules/cool-ui";
type Form = {
val1: string;
val2: string;
val3: string;
val4: string;
val5: string;
val6: string;
};
const form = reactive<Form>({
val1: "1",
val2: "2",
val3: "1",
val4: "1",
val5: "2",
val6: "1"
});
const list = ref<ClTabsItem[]>([
{
label: "Vue",
value: "1"
},
{
label: "React",
value: "2"
},
{
label: "Angular",
value: "3"
},
{
label: "Svelte",
value: "4"
},
{
label: "Jquery",
value: "5"
},
{
label: "Vuex",
value: "6"
},
{
label: "Vue Router",
value: "7"
},
{
label: "Pinia",
value: "8"
}
]);
const list2 = ref<ClTabsItem[]>([
{
label: "Vue",
value: "1"
},
{
label: "React",
value: "2"
},
{
label: "Angular",
value: "3"
},
{
label: "Svelte",
value: "4"
}
]);
const list3 = ref<ClTabsItem[]>([
{
label: "Vue",
value: "1"
},
{
label: "React",
value: "2"
},
{
label: "Angular",
value: "3",
disabled: true
},
{
label: "Svelte",
value: "4"
}
]);
const isShowLine = ref(true);
const isDisabled = ref(false);
const isColor = ref(false);
const isPad = ref(false);
</script>

View File

@@ -0,0 +1,97 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-topbar :title="t('标题')"> </cl-topbar>
</demo-item>
<demo-item :label="t('插槽')">
<cl-topbar :title="t('标题')">
<template #prepend>
<cl-icon
name="heart-fill"
:size="38"
:pt="{
className: 'ml-2'
}"
></cl-icon>
</template>
<template #append>
<cl-icon name="search-line" :size="38"></cl-icon>
</template>
</cl-topbar>
</demo-item>
<demo-item :label="t('自定义颜色')">
<cl-topbar :title="t('标题')" color="white" background-color="#409EFF"> </cl-topbar>
</demo-item>
<demo-item :label="t('使用 PT 自定义颜色')">
<cl-topbar
:title="t('标题')"
:pt="{
className: 'bg-surface-800',
title: {
className: 'text-white'
},
back: {
className: 'text-white'
}
}"
>
</cl-topbar>
</demo-item>
<demo-item :label="t('自定义返回图标')">
<cl-topbar
:title="t('标题')"
back-icon="home-2-line"
:pt="{
back: {
size: 38
}
}"
>
</cl-topbar>
</demo-item>
<demo-item :label="t('禁用返回按钮')">
<cl-topbar :title="t('标题')" :backable="false"> </cl-topbar>
</demo-item>
<demo-item :label="t('自定义返回路径')">
<cl-topbar :title="t('标题')" back-path="/pages/user/login">
<template #prepend>
<cl-text>{{ t("登录") }}</cl-text>
</template>
</cl-topbar>
</demo-item>
<demo-item :label="t('自定义标题内容')">
<cl-topbar>
<cl-tabs v-model="type" :height="66" :list="typeList"></cl-tabs>
</cl-topbar>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import type { ClTabsItem } from "@/uni_modules/cool-ui";
const type = ref("fans");
const typeList = ref<ClTabsItem[]>([
{
label: "我的粉丝",
value: "fans"
},
{
label: "我的关注",
value: "follow"
}
]);
</script>

View File

@@ -0,0 +1,283 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础动画')">
<view class="row">
<view class="item" ref="rotateRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">rotate</cl-text>
</view>
<view class="item" ref="scaleRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">scale</cl-text>
</view>
<view class="item" ref="moveRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">move</cl-text>
</view>
<view class="item" ref="opacityRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">opacity</cl-text>
</view>
</view>
</demo-item>
<demo-item :label="t('淡入淡出')">
<view class="row">
<view class="item" ref="fadeInRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">fadeIn</cl-text>
</view>
<view class="item" ref="fadeOutRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">fadeOut</cl-text>
</view>
</view>
<cl-button text border @tap="onFadeToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
<demo-item :label="t('滑入')">
<view class="row">
<view class="item" ref="slideLeftRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">slideLeft</cl-text>
</view>
<view class="item" ref="slideRightRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">slideRight</cl-text>
</view>
<view class="item" ref="slideUpRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">slideUp</cl-text>
</view>
<view class="item" ref="slideDownRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">slideDown</cl-text>
</view>
</view>
<cl-button text border @tap="onSlideInToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
<demo-item label="缩放">
<view class="row">
<view class="item" ref="zoomInRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">zoomIn</cl-text>
</view>
<view class="item" ref="zoomOutRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">zoomOut</cl-text>
</view>
</view>
<cl-button text border @tap="onZoomToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
<demo-item :label="t('旋转翻转')">
<view class="row">
<view class="item" ref="rotateInRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">rotateIn</cl-text>
</view>
<view class="item" ref="flipXRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">flipX</cl-text>
</view>
<view class="item" ref="flipYRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">flipY</cl-text>
</view>
</view>
<cl-button text border @tap="onRotateFlipToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
<demo-item :label="t('摇摆抖动')">
<view class="row">
<view class="item" ref="shakeRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">shake</cl-text>
</view>
<view class="item" ref="swingRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">swing</cl-text>
</view>
<view class="item" ref="wobbleRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">wobble</cl-text>
</view>
</view>
<cl-button text border @tap="onShakeToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
<demo-item :label="t('特殊效果')">
<view class="row">
<view class="item" ref="rollInRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">rollIn</cl-text>
</view>
<view class="item" ref="lightSpeedRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">lightSpeed</cl-text>
</view>
<view class="item" ref="rippleRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">ripple</cl-text>
</view>
</view>
<cl-button text border @tap="onSpecialToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
<demo-item :label="t('组合动画')">
<view class="row">
<view class="item" ref="sequenceRef">
<cl-text color="white" :pt="{ className: 'text-sm' }">sequence</cl-text>
</view>
</view>
<cl-button text border @tap="onSequenceToggle">{{ t("播放动画") }}</cl-button>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { AnimationEngine, createAnimation } from "@/cool";
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
// 基础动画引用
const rotateRef = ref<UniElement | null>(null);
const moveRef = ref<UniElement | null>(null);
const scaleRef = ref<UniElement | null>(null);
const opacityRef = ref<UniElement | null>(null);
// 淡入淡出动画引用
const fadeInRef = ref<UniElement | null>(null);
const fadeOutRef = ref<UniElement | null>(null);
// 滑入动画引用
const slideLeftRef = ref<UniElement | null>(null);
const slideRightRef = ref<UniElement | null>(null);
const slideUpRef = ref<UniElement | null>(null);
const slideDownRef = ref<UniElement | null>(null);
// 缩放动画引用
const zoomInRef = ref<UniElement | null>(null);
const zoomOutRef = ref<UniElement | null>(null);
// 旋转翻转动画引用
const rotateInRef = ref<UniElement | null>(null);
const flipXRef = ref<UniElement | null>(null);
const flipYRef = ref<UniElement | null>(null);
// 摇摆抖动动画引用
const shakeRef = ref<UniElement | null>(null);
const swingRef = ref<UniElement | null>(null);
const wobbleRef = ref<UniElement | null>(null);
// 特殊效果动画引用
const rollInRef = ref<UniElement | null>(null);
const lightSpeedRef = ref<UniElement | null>(null);
const rippleRef = ref<UniElement | null>(null);
// 组合动画引用
const sequenceRef = ref<UniElement | null>(null);
// 基础动画方法
function onRotate() {
createAnimation(rotateRef.value, {
duration: 1000,
loop: -1
})
.rotate("0deg", "360deg")
.play();
}
function onMove() {
createAnimation(moveRef.value, {
duration: 1000,
loop: -1,
alternate: true
})
.translateX("0rpx", "300rpx")
.play();
}
function onScale() {
createAnimation(scaleRef.value, {
duration: 500,
loop: -1,
alternate: true
})
.scale("1", "1.2")
.play();
}
function onOpacity() {
createAnimation(opacityRef.value, {
duration: 500,
loop: -1,
alternate: true
})
.opacity("1", "0.5")
.play();
}
// 淡入淡出动画方法
function onFadeToggle() {
createAnimation(fadeInRef.value).fadeIn(500).play();
createAnimation(fadeOutRef.value).fadeOut(500).play();
}
// 滑入动画方法
function onSlideInToggle() {
createAnimation(slideLeftRef.value).slideInLeft(400).play();
createAnimation(slideRightRef.value).slideInRight(400).play();
createAnimation(slideUpRef.value).slideInUp(400).play();
createAnimation(slideDownRef.value).slideInDown(400).play();
}
// 缩放动画方法
function onZoomToggle() {
createAnimation(zoomInRef.value).zoomIn(400).play();
createAnimation(zoomOutRef.value).zoomOut(400).play();
}
// 旋转翻转动画方法
function onRotateFlipToggle() {
createAnimation(rotateInRef.value).rotateIn(600).play();
createAnimation(flipXRef.value).flipX(600).play();
createAnimation(flipYRef.value).flipY(600).play();
}
// 摇摆抖动动画方法
function onShakeToggle() {
createAnimation(shakeRef.value).shake(200).play();
createAnimation(swingRef.value).swing(200).play();
createAnimation(wobbleRef.value).wobble(200).play();
}
// 特殊效果动画方法
function onSpecialToggle() {
createAnimation(rollInRef.value).rollIn(600).play();
createAnimation(lightSpeedRef.value).lightSpeed(500).play();
createAnimation(rippleRef.value).ripple(600).play();
}
// 异步序列动画方法
async function onSequenceToggle() {
createAnimation(sequenceRef.value)
.sequence([
(engine: AnimationEngine) => engine.fadeIn(300),
(engine: AnimationEngine) => engine.scale("1", "1.5").setDuration(400),
(engine: AnimationEngine) => engine.rotate("0deg", "360deg").setDuration(500),
(engine: AnimationEngine) => engine.scale("1.5", "1").setDuration(300),
(engine: AnimationEngine) => engine.fadeOut(300)
])
.play();
}
onReady(() => {
onRotate();
onMove();
onScale();
onOpacity();
});
</script>
<style lang="scss" scoped>
.row {
@apply flex flex-row justify-between items-center overflow-visible;
margin: 0 -10rpx 20rpx -10rpx;
.item {
@apply flex items-center justify-center h-16 bg-primary-500 rounded-xl flex-1;
margin: 0 10rpx;
}
}
</style>

View File

@@ -0,0 +1,233 @@
<template>
<cl-page>
<cl-canvas
v-if="width > 0"
ref="canvasRef"
canvas-id="test"
:height="height"
:width="width"
@load="onCanvasLoad"
></cl-canvas>
<cl-footer>
<!-- #ifdef H5 -->
<cl-button type="primary" @click="previewImage">{{ t("预览图片") }}</cl-button>
<!-- #endif -->
<!-- #ifndef H5 -->
<cl-button type="primary" @click="saveImage">{{ t("保存图片") }}</cl-button>
<!-- #endif -->
</cl-footer>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import { Canvas } from "@/uni_modules/cool-canvas";
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const canvasRef = ref<ClCanvasComponentPublicInstance | null>(null);
// 初始化为 0避免页面未完全渲染时 getWindowInfo 获取到的高度或宽度不准确(如已知固定高宽可直接赋值)
const height = ref(0);
const width = ref(0);
function onCanvasLoad(canvas: Canvas) {
canvas
.image({
x: 0,
y: 0,
width: width.value,
height: height.value,
url: "/static/demo/canvas/bg.png"
})
.image({
x: 0,
y: 0,
width: width.value,
height: height.value,
url: "/static/demo/canvas/light.png"
})
.image({
x: (width.value - 226) / 2,
y: 60,
width: 226,
height: 77,
url: "/static/demo/canvas/text-yqhy.png"
})
.image({
x: (width.value - 325) / 2,
y: 125,
width: 325,
height: 77,
url: "/static/demo/canvas/text-dezk.png"
})
.image({
x: (width.value - 196) / 2,
y: 190,
width: 196,
height: 62,
url: "/static/demo/canvas/text-xrfl.png"
})
.image({
x: (width.value - 374) / 2 - 1,
y: 500,
width: 374,
height: 220,
url: "/static/demo/canvas/rp-t.png"
})
.image({
x: (width.value - 327) / 2,
y: 280,
width: 327,
height: 430,
url: "/static/demo/canvas/bg-content.png"
})
.image({
x: 30,
y: 240,
width: 114,
height: 120,
url: "/static/demo/canvas/gold-l.png"
})
.image({
x: width.value - 106 - 50,
y: 240,
width: 106,
height: 107,
url: "/static/demo/canvas/gold-r.png"
})
.image({
x: (width.value - 350) / 2,
y: 595,
width: 350,
height: 122,
url: "/static/demo/canvas/rp-b.png"
})
.image({
x: (width.value - 208) / 2,
y: 631,
width: 208,
height: 89,
url: "/static/demo/canvas/invite-btn.png"
})
.div({
x: (width.value - 276) / 2,
y: 335,
width: 276,
height: 210,
backgroundColor: "#fff",
radius: 10
})
.text({
x: 0,
y: 350,
width: width.value,
content: "新人专享",
color: "#F73035",
textAlign: "center",
fontSize: 28,
fontWeight: "bold"
})
.text({
x: 0,
y: 390,
width: width.value,
content: "限时领券30元",
color: "#F73035",
textAlign: "center",
fontSize: 16
})
.image({
x: (width.value - 246) / 2,
y: 432,
width: 246,
height: 98,
url: "/static/demo/canvas/coupon.png"
})
.text({
x: (width.value - 246) / 2,
y: 435,
content: "领券",
width: 34,
color: "#fff",
fontSize: 11,
textAlign: "center"
})
.text({
x: (width.value - 246) / 2,
y: 454,
width: 86,
content: "80",
color: "#604E44",
fontSize: 46,
textAlign: "center",
fontWeight: "bold"
})
.text({
x: (width.value - 246) / 2 + 86,
y: 459,
width: 246 - 86,
content: "新人专享优惠券",
color: "#604E44",
fontSize: 18,
textAlign: "center"
})
.text({
x: (width.value - 246) / 2 + 86,
y: 489,
width: 246 - 86,
content: "2021.04.30-2021.06.30",
color: "#604E44",
fontSize: 12,
textAlign: "center"
})
.text({
x: 0,
y: 560,
width: width.value,
content: "邀请好友双方均可获得20元优惠券",
color: "#756056",
fontSize: 15,
textAlign: "center"
})
.text({
x: 0,
y: 300,
width: width.value,
content: " 专属新人福利 ",
color: "#956056",
textAlign: "center",
opacity: 0.7
})
.draw();
}
function previewImage() {
canvasRef.value!.previewImage();
}
function saveImage() {
canvasRef.value!.saveImage();
}
onReady(() => {
// 同上
height.value = uni.getWindowInfo().windowHeight;
width.value = uni.getWindowInfo().windowWidth;
ui.showConfirm({
title: t("提示"),
message: t("本页面内容由 canvas 渲染生成,是否立即预览图片效果?"),
confirmText: t("预览"),
callback(action) {
if (action == "confirm") {
canvasRef.value!.previewImage();
}
}
});
});
</script>

View File

@@ -0,0 +1,55 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义')">
<cl-button @tap="chooseImage">{{ t("选择图片") }}</cl-button>
<cl-list border :pt="{ className: 'mt-5' }">
<cl-list-item :label="t('可调节裁剪框大小')">
<cl-switch v-model="resizable"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
<cl-cropper
ref="cropperRef"
:resizable="resizable"
@crop="onCrop"
@load="onImageLoad"
></cl-cropper>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
const cropperRef = ref<ClCropperComponentPublicInstance | null>(null);
const resizable = ref(true);
function chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success: (res) => {
if (res.tempFilePaths.length > 0) {
cropperRef.value!.open(res.tempFilePaths[0]);
}
}
});
}
function onCrop(url: string) {
uni.previewImage({
urls: [url]
});
}
function onImageLoad(e: UniImageLoadEvent) {
console.log("onImageLoad", e);
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('格式化')">
<cl-text>format("YYYY-MM-DD HH:mm:ss")</cl-text>
</demo-item>
<demo-item :label="t('添加')">
<cl-text>add(1, "day")</cl-text>
</demo-item>
<demo-item :label="t('减去')">
<cl-text>subtract(1, "day")</cl-text>
</demo-item>
<demo-item :label="t('获取某个单位的开始时间')">
<cl-text>startOf("day")</cl-text>
</demo-item>
<demo-item :label="t('获取某个单位的结束时间')">
<cl-text>endOf("month")</cl-text>
</demo-item>
<demo-item :label="t('是否同一天')">
<cl-text>isSame(Date)</cl-text>
</demo-item>
<demo-item :label="t('是否早于')">
<cl-text>isBefore(Date)</cl-text>
</demo-item>
<demo-item :label="t('是否晚于')">
<cl-text>isAfter(Date)</cl-text>
</demo-item>
<demo-item :label="t('差值')">
<cl-text>diff(Date)</cl-text>
</demo-item>
<demo-item :label="t('差值(单位)')">
<cl-text>diffUnit(Date, "day")</cl-text>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,88 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义')">
<view class="flex flex-row justify-center py-3">
<cl-qrcode
ref="qrcodeRef"
text="https://cool-js.com/"
:logo="isLogo ? 'https://unix.cool-js.com/logo.png' : ''"
:pd-radius="isPdRadius ? 50 : 0"
:padding="isPadding ? 10 : 5"
:foreground="isColor ? '#14b8a6' : '#000000'"
:pd-color="isColor ? '#0d9488' : '#000000'"
:mode="mode"
></cl-qrcode>
</view>
<cl-list border class="mt-3">
<cl-list-item :label="t('添加LOGO')">
<cl-switch v-model="isLogo"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('圆角定位点')">
<cl-switch v-model="isPdRadius"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('内间距')">
<cl-switch v-model="isPadding"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('导出图片')">
<cl-button @tap="save">{{ t("预览") }}</cl-button>
</cl-list-item>
<view class="p-2">
<cl-tabs v-model="mode" :height="66" :list="modeList" show-slider></cl-tabs>
</view>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import type { ClQrcodeMode, ClTabsItem } from "@/uni_modules/cool-ui";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { t } from "@/locale";
const isLogo = ref(true);
const isPdRadius = ref(true);
const isPadding = ref(false);
const isColor = ref(false);
const qrcodeRef = ref<ClQrcodeComponentPublicInstance | null>(null);
const mode = ref<ClQrcodeMode>("circular");
const modeList = [
{
label: t("矩形"),
value: "rect"
},
{
label: t("点"),
value: "circular"
},
{
label: t("线性"),
value: "line"
},
{
label: t("小方格"),
value: "rectSmall"
}
] as ClTabsItem[];
function save() {
qrcodeRef.value!.toPng().then((url) => {
uni.previewImage({
urls: [url]
});
});
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('跳转')">
<cl-button @click="toPush">{{ t("跳转") }}</cl-button>
</demo-item>
<demo-item :label="t('带参数')">
<cl-button @click="toQuery">{{ t("跳转") }}</cl-button>
</demo-item>
<demo-item :label="t('需登录')">
<cl-button @click="toLogin">{{ t("跳转") }}</cl-button>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { router, uuid } from "@/cool";
import DemoItem from "../../components/item.uvue";
import { t } from "@/locale";
function toPush() {
router.to("/pages/demo/other/router/query");
}
function toQuery() {
router.push({ path: "/pages/demo/other/router/query", query: { id: uuid() } });
}
function toLogin() {
router.push({
path: "/pages/demo/other/router/query",
query: { id: uuid() },
isAuth: true
});
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,34 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('ID')">
<cl-text>{{ id ?? "-" }}</cl-text>
</demo-item>
<demo-item :label="t('用户信息')" v-if="!user.isNull()">
<cl-text>{{ userInfo?.nickName ?? "-" }}</cl-text>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../../components/item.uvue";
import { router, userInfo, useStore } from "@/cool";
const { user } = useStore();
const props = defineProps({
id: {
type: String
}
});
onReady(() => {
const query = router.query();
console.log(query);
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,83 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('分享文本')">
<cl-button @tap="shareText">{{ t("分享文本") }}</cl-button>
</demo-item>
<demo-item :label="t('分享图片')">
<cl-button @tap="shareImage">{{ t("分享图片") }}</cl-button>
</demo-item>
<demo-item :label="t('分享文件')">
<cl-button @tap="shareFile">{{ t("分享文件") }}</cl-button>
</demo-item>
<demo-item :label="t('分享链接')">
<cl-button @tap="shareLink">{{ t("分享链接") }}</cl-button>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
// #ifdef APP
import { shareWithSystem } from "@/uni_modules/cool-share";
// #endif
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
function shareText() {
// #ifdef APP
shareWithSystem({
type: "text",
title: "Cool Unix",
summary: "Cool Unix 是一个高效的项目脚手架",
success: () => {
console.log("success");
},
fail: (error) => {
console.log("fail", error);
}
});
// #endif
}
function shareImage() {
// #ifdef APP
shareWithSystem({
type: "image",
url: "https://cool-js.com/logo.png",
success: () => {
console.log("success");
}
});
// #endif
}
function shareFile() {
// #ifdef APP
shareWithSystem({
type: "file",
url: "https://show.cool-admin.com/用户导入模版.xlsx",
success: () => {
console.log("success");
}
});
// #endif
}
function shareLink() {
// #ifdef APP
shareWithSystem({
type: "link",
url: "https://cool-js.com/",
success: () => {
console.log("success");
}
});
// #endif
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,54 @@
<template>
<cl-page>
<cl-sign ref="signRef" :height="height" :width="width" :enable-brush="isBrush"></cl-sign>
<view class="p-3">
<cl-list>
<cl-list-item :label="t('操作')">
<cl-button type="info" @click="clear">{{ t("清空") }}</cl-button>
<cl-button @click="preview">{{ t("预览") }}</cl-button>
</cl-list-item>
<cl-list-item :label="t('设置高度')">
<cl-switch v-model="isFullscreen" @change="onFullscreenChange"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('毛笔效果')">
<cl-switch v-model="isBrush"></cl-switch>
</cl-list-item>
</cl-list>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { t } from "@/locale";
import { ref } from "vue";
const height = ref(200);
const width = ref(0);
const isFullscreen = ref(false);
const isBrush = ref(true);
const signRef = ref<ClSignComponentPublicInstance | null>(null);
function clear() {
signRef.value!.clear();
}
function preview() {
signRef.value!.toPng().then((url) => {
uni.previewImage({
urls: [url]
});
});
}
function onFullscreenChange() {
height.value = isFullscreen.value ? uni.getWindowInfo().windowHeight - 200 : 200;
}
onReady(() => {
width.value = uni.getWindowInfo().windowWidth;
});
</script>

View File

@@ -0,0 +1,47 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-slide-verify
v-model="status"
@success="onSuccess"
@fail="onFail"
></cl-slide-verify>
</demo-item>
<demo-item :label="t('没有错误提示')">
<cl-slide-verify :show-fail="false"></cl-slide-verify>
</demo-item>
<demo-item :label="t('转动图片')">
<cl-slide-verify
mode="image"
image-url="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-slide-verify>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const status = ref(false);
function onSuccess() {
ui.showToast({
message: t("验证通过")
});
}
function onFail() {
ui.showToast({
message: t("验证失败")
});
}
</script>

View File

@@ -0,0 +1,71 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<view class="flex flex-row">
<cl-svg src="/static/demo/svg/category.svg" class="h-6 w-6"></cl-svg>
<cl-svg src="/static/demo/svg/shopping-cart.svg" class="h-6 w-6 ml-3"></cl-svg>
<cl-svg src="/static/demo/svg/points.svg" class="h-6 w-6 ml-3"></cl-svg>
</view>
</demo-item>
<demo-item :label="t('不同大小')">
<view class="flex flex-row">
<cl-svg src="/static/demo/svg/points.svg" class="h-10 w-10"></cl-svg>
<cl-svg src="/static/demo/svg/points.svg" class="h-8 w-8 ml-3"></cl-svg>
<cl-svg src="/static/demo/svg/points.svg" class="h-6 w-6 ml-3"></cl-svg>
</view>
</demo-item>
<demo-item :label="t('不同颜色')">
<view class="flex flex-row">
<!-- #ifdef MP -->
<cl-svg :src="svg" color="primary" class="h-6 w-6"></cl-svg>
<cl-svg :src="svg" color="#d946ef" class="h-6 w-6 ml-3"></cl-svg>
<!-- #endif -->
<!-- #ifndef MP -->
<cl-svg
src="/static/demo/svg/category.svg"
color="primary"
class="h-6 w-6"
></cl-svg>
<cl-svg
src="/static/demo/svg/shopping-cart.svg"
color="#eab308"
class="h-6 w-6 ml-3"
></cl-svg>
<cl-svg
src="/static/demo/svg/points.svg"
color="#a855f7"
class="h-6 w-6 ml-3"
></cl-svg>
<!-- #endif -->
</view>
</demo-item>
<!-- #ifdef APP-ANDROID || H5 -->
<demo-item :label="t('使用base64')">
<view class="flex flex-row">
<cl-svg :src="base64" class="h-6 w-6"></cl-svg>
</view>
</demo-item>
<!-- #endif -->
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
const svg = ref(
`<svg t="1756119341770" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7779" width="64" height="64"><path d="M783.1 899.3H242.9c-97.7 0-177.3-79.5-177.3-177.3V302.6c0-97.7 79.5-177.3 177.3-177.3H783c97.7 0 177.3 79.5 177.3 177.3V722c0.1 97.7-79.5 177.3-177.2 177.3zM242.9 214.8c-48.4 0-87.8 39.4-87.8 87.8V722c0 48.4 39.4 87.8 87.8 87.8H783c48.4 0 87.8-39.4 87.8-87.8V302.6c0-48.4-39.4-87.8-87.8-87.8H242.9z" fill="#333333" p-id="7780"></path><path d="M513 600.5c-24.9 0-49.9-7.3-71.6-21.8l-2.9-2.1-214.9-170.1c-19.4-15.3-22.7-43.5-7.3-62.8 15.3-19.4 43.5-22.6 62.8-7.3l213.2 168.8c12.7 7.8 28.7 7.8 41.5 0L747 336.4c19.3-15.3 47.5-12.1 62.8 7.3 15.3 19.4 12.1 47.5-7.3 62.8L584.6 578.7c-21.7 14.5-46.7 21.8-71.6 21.8z" fill="#333333" p-id="7781"></path></svg>`
);
const base64 = ref(
`data:image/svg+xml;base64,PHN2ZyB0PSIxNzU2MTE2OTQxMjk0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijc2MTYiIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHBhdGggZD0iTTU0MS44IDkyOC41aC02MS4xYy01MC42IDAtOTEuOC00MS4yLTkxLjgtOTEuOFY2MTMuNmMwLTUwLjYgNDEuMi05MS44IDkxLjgtOTEuOGg2MS4xYzUwLjYgMCA5MS44IDQxLjIgOTEuOCA5MS44djIyMy4xYzAgNTAuNy00MS4xIDkxLjgtOTEuOCA5MS44eiBtLTYxLjEtMzE2LjJsLTEuMyAyMjQuNCA2Mi41IDEuM2MwLjcgMCAxLjMtMC42IDEuMy0xLjNWNjEzLjZsLTYyLjUtMS4zek04MzQuOSA5MjguNWgtNjEuMWMtNTAuNiAwLTkxLjgtNDEuMi05MS44LTkxLjhWNDA5LjVjMC01MC42IDQxLjItOTEuOCA5MS44LTkxLjhoNjEuMWM1MC42IDAgOTEuOCA0MS4yIDkxLjggOTEuOHY0MjcuM2MwIDUwLjYtNDEuMiA5MS43LTkxLjggOTEuN3ogbS02MS4yLTUyMC40bC0xLjMgNDI4LjYgNjIuNSAxLjNjMC43IDAgMS4zLTAuNiAxLjMtMS4zVjQwOS41bC02Mi41LTEuNHpNMjUyLjQgOTI4LjVoLTYxLjFjLTUwLjYgMC05MS44LTQxLjItOTEuOC05MS44di04MC4yYzAtNTAuNiA0MS4yLTkxLjggOTEuOC05MS44aDYxLjFjNTAuNiAwIDkxLjggNDEuMiA5MS44IDkxLjh2ODAuMmMwIDUwLjctNDEuMiA5MS44LTkxLjggOTEuOHogbS02MS4xLTE3My4zbC0xLjMgODEuNSA2Mi41IDEuM2MwLjcgMCAxLjMtMC42IDEuMy0xLjN2LTgwLjJsLTYyLjUtMS4zek0xNDQuNiA2MjUuNWMtMTEuNiAwLTIzLjItNC40LTMyLTEzLjMtMTcuNy0xNy43LTE3LjYtNDYuMyAwLTY0TDUyNiAxMzUuM2MxNy43LTE3LjYgNDYuMy0xNy42IDY0IDAgMTcuNyAxNy43IDE3LjYgNDYuMyAwIDY0TDE3Ni42IDYxMi4yYy04LjkgOC44LTIwLjUgMTMuMy0zMiAxMy4zeiIgZmlsbD0iIzMzMzMzMyIgcC1pZD0iNzYxNyI+PC9wYXRoPjxwYXRoIGQ9Ik01ODguNCAzNjQuN2MtMjUgMC00NS4yLTIwLjMtNDUuMi00NS4yVjIxOC45YzAtMjAuMy0xNi41LTM2LjgtMzYuOC0zNi44SDQwNS44Yy0yNSAwLTQ1LjItMjAuMy00NS4yLTQ1LjJzMjAuMy00NS4yIDQ1LjItNDUuMmgxMDAuNmM3MC4yIDAgMTI3LjIgNTcuMSAxMjcuMiAxMjcuMnYxMDAuNmMwIDI1LTIwLjIgNDUuMi00NS4yIDQ1LjJ6IiBmaWxsPSIjMzMzMzMzIiBwLWlkPSI3NjE4Ij48L3BhdGg+PC9zdmc+`
);
</script>

View File

@@ -0,0 +1,19 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-button @tap="onVibrate">{{ t("点击触发") }}</cl-button>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { vibrate } from "@/uni_modules/cool-vibrate";
function onVibrate() {
vibrate(1);
}
</script>

View File

@@ -0,0 +1,147 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义样式')">
<view class="flex">
<cl-watermark
:text="customText"
:font-size="fontSize"
:color="color"
:dark-color="darkColor"
:opacity="opacity"
:rotate="rotate"
:width="width"
:height="height"
:gap-x="gapX"
:gap-y="gapY"
:font-weight="fontWeight"
>
<view
class="flex flex-col p-4 rounded-xl bg-surface-50 dark:bg-surface-800 h-[400rpx]"
>
<cl-text>
明月几时有?把酒问青天。 不知天上宫阙,今夕是何年。
我欲乘风归去,又恐琼楼玉宇,高处不胜寒。 起舞弄清影,何似在人间。
</cl-text>
</view>
</cl-watermark>
</view>
<cl-list border class="mt-3">
<cl-list-item :label="t('水印文本')">
<cl-input
v-model="customText"
placeholder="请输入水印文本"
:pt="{ className: 'w-[300rpx]' }"
/>
</cl-list-item>
<cl-list-item :label="t('字体大小')">
<view class="w-[300rpx]">
<cl-slider v-model="fontSize" :min="12" :max="32" :step="1"></cl-slider>
</view>
</cl-list-item>
<!-- #ifndef APP -->
<cl-list-item :label="t('透明度')">
<view class="w-[300rpx]">
<cl-slider
v-model="opacity"
:min="0.1"
:max="1"
:step="0.05"
></cl-slider>
</view>
</cl-list-item>
<!-- #endif -->
<cl-list-item :label="t('旋转角度')">
<view class="w-[300rpx]">
<cl-slider
v-model="rotate"
:min="-180"
:max="180"
:step="5"
></cl-slider>
</view>
</cl-list-item>
<cl-list-item :label="t('水印宽度')">
<view class="w-[300rpx]">
<cl-slider v-model="width" :min="80" :max="300" :step="10"></cl-slider>
</view>
</cl-list-item>
<cl-list-item :label="t('水印高度')">
<view class="w-[300rpx]">
<cl-slider v-model="height" :min="40" :max="200" :step="10"></cl-slider>
</view>
</cl-list-item>
<cl-list-item :label="t('水平间距')">
<view class="w-[300rpx]">
<cl-slider v-model="gapX" :min="20" :max="200" :step="10"></cl-slider>
</view>
</cl-list-item>
<cl-list-item :label="t('垂直间距')">
<view class="w-[300rpx]">
<cl-slider v-model="gapY" :min="20" :max="200" :step="10"></cl-slider>
</view>
</cl-list-item>
<cl-list-item :label="t('字体粗细')">
<cl-tabs
v-model="fontWeight"
:list="fontWeightList"
:height="60"
show-slider
></cl-tabs>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('图片保护')">
<view class="flex">
<cl-watermark text="© Cool UI" :width="200" :height="80" :opacity="0.9">
<image
src="https://unix.cool-js.com/images/demo/avatar.jpg"
mode="aspectFit"
class="w-full"
></image>
</cl-watermark>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { t } from "@/locale";
import type { ClTabsItem } from "@/uni_modules/cool-ui";
const customText = ref("Cool UI");
const fontSize = ref(16);
const color = ref("rgba(0, 0, 0, 0.15)");
const darkColor = ref("rgba(255, 255, 255, 0.15)");
const opacity = ref(1);
const rotate = ref(-22);
const width = ref(120);
const height = ref(64);
const gapX = ref(100);
const gapY = ref(100);
const fontWeight = ref("normal");
const fontWeightList = [
{
label: t("正常"),
value: "normal"
},
{
label: t("加粗"),
value: "bold"
}
] as ClTabsItem[];
</script>

View File

@@ -0,0 +1,80 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<view class="flex flex-row overflow-visible">
<cl-badge type="primary" value="1" class="mr-2"></cl-badge>
<cl-badge type="success" value="12" class="mr-2"></cl-badge>
<cl-badge type="warn" value="31" class="mr-2"></cl-badge>
<cl-badge type="error" value="24" class="mr-2"></cl-badge>
<cl-badge type="info" value="17" class="mr-2"></cl-badge>
<cl-badge type="primary" value="41" class="mr-2"></cl-badge>
<cl-badge type="success" value="56" class="mr-2"></cl-badge>
</view>
</demo-item>
<demo-item :label="t('结合按钮')">
<view class="flex flex-row overflow-visible">
<cl-button>
{{ t("购买") }}
<template #content>
<cl-badge type="error" value="1" position> </cl-badge>
</template>
</cl-button>
<cl-button :pt="{ className: '!ml-5' }">
{{ t("消息") }}
<template #content>
<cl-badge type="error" dot position> </cl-badge>
</template>
</cl-button>
</view>
</demo-item>
<demo-item :label="t('结合图片')">
<view class="flex flex-row overflow-visible">
<cl-image
:pt="{
className: 'overflow-visible'
}"
src="https://unix.cool-js.com/images/demo/bg1.png"
>
<cl-badge type="error" value="+9" position> </cl-badge>
</cl-image>
</view>
</demo-item>
<demo-item :label="t('结合图标')">
<view class="flex flex-row overflow-visible">
<view class="relative overflow-visible">
<cl-icon name="mail-line"> </cl-icon>
<cl-badge
type="error"
dot
position
:pt="{
className: '!top-[-6rpx] !right-[-6rpx]'
}"
>
</cl-badge>
</view>
</view>
</demo-item>
<demo-item :label="t('自定义样式')">
<view class="flex flex-row overflow-visible">
<cl-badge
type="info"
:pt="{ className: '!rounded-bl-none' }"
value="1"
></cl-badge>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,84 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-countdown :datetime="datetime"></cl-countdown>
</demo-item>
<demo-item :label="t('隐藏为 00 的值')">
<cl-countdown :minute="60" hide-zero></cl-countdown>
</demo-item>
<demo-item :label="t('指定天数')">
<cl-countdown :day="2" format="{d}天{h}:{m}:{s}"></cl-countdown>
</demo-item>
<demo-item :label="t('自定义模板')">
<cl-countdown :day="1" format="{d}天{h}时{m}分{s}秒"></cl-countdown>
</demo-item>
<demo-item :label="t('指定小时')">
<cl-countdown :hour="2"></cl-countdown>
</demo-item>
<demo-item :label="t('指定分钟')">
<cl-countdown :minute="2"></cl-countdown>
</demo-item>
<demo-item :label="t('指定秒')">
<cl-countdown :second="10"></cl-countdown>
</demo-item>
<demo-item :label="t('完成后提示')">
<cl-countdown :second="5" @done="onDone"></cl-countdown>
</demo-item>
<demo-item :label="t('3秒后开始倒计时')">
<cl-countdown ref="countdownRef" :second="5" :auto="false"></cl-countdown>
</demo-item>
<demo-item :label="t('自定义样式')">
<cl-countdown
:hour="10"
:pt="{
text: {
className: parseClass([
'px-2 py-1 rounded-md',
[isDark, 'bg-surface-700', 'bg-surface-100']
])
},
splitor: {
className: 'px-2'
}
}"
></cl-countdown>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { onMounted, ref } from "vue";
import { dayUts, isDark, parseClass } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const datetime = ref(dayUts().add(1, "minute").toDate());
function onDone() {
ui.showToast({
message: "倒计时完成"
});
}
const countdownRef = ref<ClCountdownComponentPublicInstance | null>(null);
onMounted(() => {
setTimeout(() => {
countdownRef.value!.next();
}, 3000);
});
</script>

View File

@@ -0,0 +1,29 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-loadmore loading></cl-loadmore>
</demo-item>
<demo-item :label="t('3秒后加载完成')">
<cl-loadmore :loading="loading" :finish="finish"></cl-loadmore>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { onMounted, ref } from "vue";
const loading = ref(true);
const finish = ref(false);
onMounted(() => {
setTimeout(() => {
loading.value = false;
finish.value = true;
}, 3000);
});
</script>

View File

@@ -0,0 +1,46 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-noticebar
text="云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。"
></cl-noticebar>
</demo-item>
<demo-item :label="t('带图标')">
<view class="flex flex-row items-center">
<cl-icon name="notification-4-line" class="mr-2"></cl-icon>
<cl-noticebar
text="云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。"
></cl-noticebar>
</view>
</demo-item>
<demo-item :label="t('设置速度')">
<cl-noticebar
:speed="40"
text="云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。"
></cl-noticebar>
</demo-item>
<demo-item :label="t('垂直方向')">
<cl-noticebar
direction="vertical"
:text="[
'江南可采莲,莲叶何田田',
'鱼戏莲叶间',
'鱼戏莲叶东',
'鱼戏莲叶西',
'鱼戏莲叶南',
'鱼戏莲叶北'
]"
></cl-noticebar>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,69 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义')">
<cl-progress-circle
:value="value"
:color="isColor ? 'red' : null"
:un-color="isColor ? '#f7bfbf' : null"
:size="isSize ? 80 : 120"
:show-text="isText"
:duration="isDuration ? 200 : 500"
></cl-progress-circle>
<cl-list
border
:pt="{
className: 'mt-5'
}"
>
<cl-list-item :label="t('改个颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示文本')">
<cl-switch v-model="isText"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('快一些')">
<cl-switch v-model="isDuration"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示文本')">
<cl-button type="light" size="small" icon="add-line" @tap="add"></cl-button>
<cl-button
type="light"
size="small"
icon="subtract-line"
@tap="sub"
></cl-button>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
const isSize = ref(false);
const isText = ref(true);
const isColor = ref(false);
const isDuration = ref(false);
const value = ref(70);
function add() {
if (value.value < 100) {
value.value += 10;
}
}
function sub() {
if (value.value > 0) {
value.value -= 10;
}
}
</script>

View File

@@ -0,0 +1,26 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-progress :value="50"></cl-progress>
</demo-item>
<demo-item :label="t('自定义颜色')">
<cl-progress :value="30" color="red" un-color="#f7bfbf"></cl-progress>
</demo-item>
<demo-item :label="t('自定义宽度')">
<cl-progress :value="30" :stroke-width="20"></cl-progress>
</demo-item>
<demo-item :label="t('不显示文本')">
<cl-progress :value="75" :show-text="false"></cl-progress>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,77 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-rolling-number :value="value"></cl-rolling-number>
<view class="flex flex-row mt-2">
<cl-button icon="add-line" size="small" @tap="add"></cl-button>
<cl-button
icon="subtract-line"
type="light"
size="small"
@tap="sub"
></cl-button>
</view>
</demo-item>
<demo-item :label="t('自定义')">
<view class="flex flex-col items-center">
<cl-rolling-number
:value="value"
:pt="{
className: parseClass([[isCustom, '!text-3xl']]),
color: isCustom ? 'primary' : ''
}"
:duration="isSpeed ? 300 : 1000"
:decimals="isDecimals ? 2 : 0"
></cl-rolling-number>
<view class="flex flex-row mt-2">
<cl-button icon="add-line" size="small" @tap="add"></cl-button>
<cl-button
icon="subtract-line"
type="light"
size="small"
@tap="sub"
></cl-button>
</view>
</view>
<cl-list border class="mt-3">
<cl-list-item :label="t('加快滚动速度')">
<cl-switch v-model="isSpeed"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示小数位数')">
<cl-switch v-model="isDecimals"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义样式')">
<cl-switch v-model="isCustom"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { parseClass } from "@/cool";
const value = ref(100);
const isSpeed = ref(false);
const isDecimals = ref(false);
const isCustom = ref(false);
function add() {
value.value += 100;
}
function sub() {
value.value -= 100;
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('文本')">
<cl-skeleton :loading="loading">
<cl-text>云想衣裳花想容,春风拂槛露华浓。</cl-text>
</cl-skeleton>
</demo-item>
<demo-item :label="t('按钮')">
<view class="flex flex-row">
<cl-skeleton type="button" :loading="loading">
<cl-button>立即购买</cl-button>
</cl-skeleton>
</view>
</demo-item>
<demo-item :label="t('图片')">
<cl-skeleton type="image" :loading="loading">
<cl-image
src="https://unix.cool-js.com/images/demo/bg1.png"
></cl-image>
</cl-skeleton>
</demo-item>
<demo-item :label="t('圆形')">
<cl-skeleton type="circle" :loading="loading">
<cl-image
:radius="100"
src="https://unix.cool-js.com/images/demo/bg1.png"
></cl-image>
</cl-skeleton>
</demo-item>
<demo-item :label="t('组合')">
<view class="flex flex-row">
<cl-skeleton type="image" loading> </cl-skeleton>
<view class="flex-1 ml-2">
<cl-skeleton type="text" loading> </cl-skeleton>
<cl-skeleton type="text" loading class="mt-2 !w-[160rpx]"> </cl-skeleton>
</view>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
const loading = ref(true);
onReady(() => {
setTimeout(() => {
loading.value = false;
}, 3000);
});
</script>

View File

@@ -0,0 +1,522 @@
<template>
<cl-page>
<cl-topbar fixed :show-back="false" safe-area-top :height="isMp() ? null : 100">
<view
class="flex flex-row items-center p-3 flex-1 w-full"
:class="{
'pt-0': isMp()
}"
>
<view class="bg-primary-500 rounded-lg p-[12rpx]">
<cl-image
src="/static/logo.png"
:width="32"
:height="32"
:show-loading="false"
></cl-image>
</view>
<cl-text
color="primary"
:pt="{
className: '!text-xl mr-auto ml-2 flex-1'
}"
>
{{ config.name }}
</cl-text>
<view
class="bg-surface-500 h-8 w-8 rounded-full flex flex-row items-center justify-center mr-3"
@tap="setSize"
>
<cl-icon name="font-size" color="white"></cl-icon>
</view>
<view
class="bg-primary-500 h-8 w-8 rounded-full flex flex-row items-center justify-center"
:class="{
'mr-24': isMp()
}"
@tap="setLocale"
>
<cl-icon name="translate" color="white"></cl-icon>
</view>
</view>
</cl-topbar>
<view class="p-3">
<view class="group" v-for="item in data" :key="item.label">
<cl-text :pt="{ className: '!text-sm !text-surface-400 mb-2 ml-2' }"
>{{ item.label }}{{ item.children?.length ?? 0 }}</cl-text
>
<view class="list">
<cl-row :gutter="10">
<template v-for="child in item.children" :key="child.label">
<cl-col :span="6" v-if="child.hidden != true">
<view
class="item dark:!bg-surface-800"
hover-class="opacity-80"
:hover-stay-time="250"
@tap="toPath(child)"
>
<cl-icon :name="child.icon" :size="36"></cl-icon>
<cl-text
:pt="{
className: 'mt-1 !text-xs text-center'
}"
>{{ child.label }}</cl-text
>
</view>
</cl-col>
</template>
</cl-row>
</view>
</view>
</view>
<!-- 自定义底部导航栏 -->
<custom-tabbar></custom-tabbar>
<!-- 主题设置 -->
<locale-set :ref="refs.set('localeSet')"></locale-set>
<!-- 字体大小设置 -->
<size-set :ref="refs.set('sizeSet')"></size-set>
</cl-page>
</template>
<script lang="ts" setup>
import { isApp, isMp, router, useRefs } from "@/cool";
import { t } from "@/locale";
import { computed } from "vue";
import { useUi } from "@/uni_modules/cool-ui";
import { config } from "@/config";
import LocaleSet from "@/components/locale-set.uvue";
import SizeSet from "@/components/size-set.uvue";
import CustomTabbar from "@/components/tabbar.uvue";
const ui = useUi();
const refs = useRefs();
type Item = {
label: string;
icon?: string;
path?: string;
disabled?: boolean;
hidden?: boolean;
children?: Item[];
};
const data = computed<Item[]>(() => {
return [
{
label: t("基础组件"),
children: [
{
label: t("文本"),
icon: "text",
path: "/pages/demo/basic/text"
},
{
label: t("按钮"),
icon: "mouse-line",
path: "/pages/demo/basic/button"
},
{
label: t("图片"),
icon: "image-line",
path: "/pages/demo/basic/image"
},
{
label: t("图标"),
icon: "puzzle-2-line",
path: "/pages/demo/basic/icon"
},
{
label: t("标签"),
icon: "price-tag-3-line",
path: "/pages/demo/basic/tag"
}
]
},
{
label: t("表单组件"),
children: [
{
label: t("表单验证"),
icon: "draft-line",
path: "/pages/demo/form/form"
},
{
label: t("输入框"),
icon: "input-field",
path: "/pages/demo/form/input"
},
{
label: t("文本域"),
icon: "text-block",
path: "/pages/demo/form/textarea"
},
{
label: t("计数器"),
icon: "increase-decrease-line",
path: "/pages/demo/form/input-number"
},
{
label: t("口令输入"),
icon: "input-method-line",
path: "/pages/demo/form/input-otp"
},
{
label: t("键盘"),
icon: "keyboard-box-line",
path: "/pages/demo/form/keyboard"
},
{
label: t("单选框"),
icon: "radio-button-line",
path: "/pages/demo/form/radio"
},
{
label: t("多选框"),
icon: "checkbox-line",
path: "/pages/demo/form/checkbox"
},
{
label: t("开关"),
icon: "toggle-line",
path: "/pages/demo/form/switch"
},
{
label: t("评分"),
icon: "star-line",
path: "/pages/demo/form/rate"
},
{
label: t("滑块"),
icon: "equalizer-2-line",
path: "/pages/demo/form/slider"
},
{
label: t("选择器"),
icon: "dropdown-list",
path: "/pages/demo/form/select"
},
{
label: t("日期选择器"),
icon: "calendar-line",
path: "/pages/demo/form/select-date"
},
{
label: t("时间选择器"),
icon: "time-line",
path: "/pages/demo/form/select-time"
},
{
label: t("级联选择器"),
icon: "stacked-view",
path: "/pages/demo/form/cascader"
},
{
label: t("文件上传"),
icon: "file-upload-line",
path: "/pages/demo/form/upload"
},
{
label: t("日历"),
icon: "calendar-line",
path: "/pages/demo/form/calendar"
}
]
},
{
label: t("布局组件"),
children: [
{
label: t("弹性布局"),
icon: "layout-2-line",
path: "/pages/demo/layout/flex"
},
{
label: t("标签页"),
icon: "function-add-line",
path: "/pages/demo/layout/tabs"
},
{
label: t("折叠面板"),
icon: "collapse-vertical-line",
path: "/pages/demo/layout/collapse"
},
{
label: t("吸顶"),
icon: "align-top",
path: "/pages/demo/layout/sticky"
},
{
label: t("导航栏"),
icon: "layout-top-line",
path: "/pages/demo/layout/topbar"
},
{
label: t("底部视图"),
icon: "layout-bottom-line",
path: "/pages/demo/layout/footer"
},
{
label: t("悬浮视图"),
icon: "picture-in-picture-line",
path: "/pages/demo/layout/float-view"
}
]
},
{
label: t("数据展示"),
children: [
{
label: t("头像"),
icon: "account-circle-line",
path: "/pages/demo/data/avatar"
},
{
label: t("查看更多"),
icon: "menu-add-line",
path: "/pages/demo/data/read-more"
},
{
label: t("列表"),
icon: "list-check",
path: "/pages/demo/data/list"
},
{
label: t("列表视图"),
icon: "list-view",
path: "/pages/demo/data/list-view"
},
{
label: t("列表刷新"),
icon: "refresh-line",
path: "/pages/demo/data/list-view-refresh",
disabled: false
},
{
label: t("瀑布流"),
icon: "layout-column-line",
path: "/pages/demo/data/waterfall"
},
{
label: t("轮播图"),
icon: "carousel-view",
path: "/pages/demo/data/banner"
},
{
label: t("跑马灯"),
icon: "film-line",
path: "/pages/demo/data/marquee"
},
{
label: t("分页"),
icon: "page-separator",
path: "/pages/demo/data/pagination"
},
{
label: t("时间轴"),
icon: "timeline-view",
path: "/pages/demo/data/timeline"
},
{
label: t("拖拽"),
icon: "drag-move-line",
path: "/pages/demo/data/draggable"
},
{
label: t("筛选栏"),
icon: "filter-line",
path: "/pages/demo/data/filter-bar"
},
{
label: t("树形结构"),
icon: "node-tree",
path: "/pages/demo/data/tree"
}
]
},
{
label: t("状态组件"),
children: [
{
label: t("角标"),
icon: "notification-badge-line",
path: "/pages/demo/status/badge"
},
{
label: t("通知栏"),
icon: "error-warning-line",
path: "/pages/demo/status/noticebar"
},
{
label: t("倒计时"),
icon: "timer-line",
path: "/pages/demo/status/countdown"
},
{
label: t("数字滚动"),
icon: "arrow-up-box-line",
path: "/pages/demo/status/rolling-number"
},
{
label: t("进度条"),
icon: "subtract-line",
path: "/pages/demo/status/progress"
},
{
label: t("圆形进度条"),
icon: "circle-line",
path: "/pages/demo/status/progress-circle"
},
{
label: t("骨架图"),
icon: "shadow-line",
path: "/pages/demo/status/skeleton"
},
{
label: t("加载更多"),
icon: "loader-4-line",
path: "/pages/demo/status/loadmore"
}
]
},
{
label: t("反馈组件"),
children: [
{
label: t("操作菜单"),
icon: "menu-line",
path: "/pages/demo/feedback/action-sheet"
},
{
label: t("弹窗"),
icon: "chat-4-line",
path: "/pages/demo/feedback/popup"
},
{
label: t("确认框"),
icon: "chat-check-line",
path: "/pages/demo/feedback/confirm"
},
{
label: t("提示框"),
icon: "message-2-line",
path: "/pages/demo/feedback/toast"
}
]
},
{
label: "其他",
children: [
{
label: "QRCode",
icon: "qr-code-line",
path: "/pages/demo/other/qrcode"
},
{
label: t("签名"),
icon: "sketching",
path: "/pages/demo/other/sign"
},
{
label: t("水印"),
icon: "copyright-line",
path: "/pages/demo/other/watermark"
},
{
label: t("图片裁剪"),
icon: "crop-line",
path: "/pages/demo/other/cropper"
},
{
label: t("Canvas"),
icon: "markup-line",
path: "/pages/demo/other/canvas"
},
{
label: t("富文本"),
icon: "text-snippet",
path: "/pages/demo/other/rict-text",
disabled: true
},
{
label: "DayUts",
icon: "timer-2-line",
path: "/pages/demo/other/day-uts"
},
{
label: "Vibrate",
icon: "volume-vibrate-line",
path: "/pages/demo/other/vibrate"
},
{
label: "SVG",
icon: "bubble-chart-line",
path: "/pages/demo/other/svg"
},
{
label: "SlideVerify",
icon: "contract-right-fill",
path: "/pages/demo/other/slide-verify"
},
{
label: "Animation",
icon: "instance-line",
path: "/pages/demo/other/animation"
},
{
label: "Router",
icon: "compass-discover-line",
path: "/pages/demo/other/router/index"
},
{
label: "Share",
icon: "share-line",
path: "/pages/demo/other/share",
hidden: !isApp()
}
]
}
];
});
function toPath(item: Item) {
if (item.disabled == true) {
return ui.showToast({
message: t("该功能正在开发中")
});
}
router.to(item.path!);
}
function setLocale() {
refs.open("localeSet");
}
function setSize() {
refs.open("sizeSet");
}
</script>
<style lang="scss" scoped>
.group {
@apply mb-10;
.list {
.item {
@apply flex flex-col items-center;
@apply rounded-xl bg-white px-2;
height: 140rpx;
margin-bottom: 10rpx;
padding-top: 36rpx;
}
}
}
</style>

View File

@@ -0,0 +1,324 @@
<template>
<cl-page>
<cl-topbar
fixed
:height="100"
:show-back="false"
safe-area-top
background-color="transparent"
>
<view class="flex flex-row items-center w-full flex-1 px-3">
<view class="top-icon dark:!bg-surface-700" @tap="toSet">
<cl-icon name="settings-line"></cl-icon>
</view>
<view class="top-icon dark:!bg-surface-700" @tap="toTest">
<cl-icon name="notification-4-line"></cl-icon>
</view>
</view>
</cl-topbar>
<view class="p-3">
<view class="flex flex-col justify-center items-center pt-6 pb-3">
<view class="relative overflow-visible" @tap="toEdit">
<cl-avatar
:src="userInfo?.avatarUrl"
:size="150"
:pt="{ className: '!rounded-3xl', icon: { size: 60 } }"
>
</cl-avatar>
<view
class="flex flex-col justify-center items-center absolute bottom-0 right-[-6rpx] bg-black rounded-full p-1"
v-if="!user.isNull()"
>
<cl-icon name="edit-line" color="white" :size="24"></cl-icon>
</view>
</view>
<view class="flex-1 flex flex-col justify-center items-center w-full" @tap="toEdit">
<cl-text :pt="{ className: '!text-xl mt-5 mb-1 font-bold' }">{{
userInfo?.nickName ?? t("未登录")
}}</cl-text>
<cl-text color="info" v-if="!user.isNull()">{{ userInfo?.phone }}</cl-text>
</view>
</view>
<cl-row
:pt="{
className: 'pt-3 pb-6'
}"
>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="171"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("总点击")
}}</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="24"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("赞")
}}</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="89"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("收藏")
}}</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="653"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("粉丝")
}}</cl-text>
</view>
</cl-col>
</cl-row>
<cl-row :gutter="20" :pt="{ className: 'mb-3' }">
<cl-col :span="12">
<view class="bg-white dark:!bg-surface-800 p-4 rounded-2xl flex flex-row">
<view class="flex flex-col mr-auto">
<cl-text
ellipsis
:pt="{
className: '!w-[180rpx]'
}"
>{{ t("接单模式") }}</cl-text
>
<cl-text :pt="{ className: '!text-xs mt-1' }" color="info">{{
t("已关闭")
}}</cl-text>
</view>
<cl-switch></cl-switch>
</view>
</cl-col>
<cl-col :span="12">
<view class="bg-white dark:!bg-surface-800 p-4 rounded-2xl flex flex-row">
<view class="flex flex-col mr-auto">
<cl-text
ellipsis
:pt="{
className: '!w-[180rpx]'
}"
>{{ t("消息通知") }}</cl-text
>
<cl-text :pt="{ className: '!text-xs mt-1' }" color="info">{{
t("已关闭")
}}</cl-text>
</view>
<cl-switch></cl-switch>
</view>
</cl-col>
</cl-row>
<view class="bg-white dark:!bg-surface-800 py-5 rounded-2xl mb-3 h-[160rpx]">
<cl-row :pt="{ className: 'overflow-visible' }">
<cl-col :span="6">
<view class="flex flex-col justify-center items-center px-2">
<cl-icon name="money-cny-circle-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("待支付") }}</cl-text
>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col justify-center items-center px-2">
<cl-icon name="box-1-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("未发货") }}</cl-text
>
</view>
</cl-col>
<cl-col :span="6">
<view
class="flex flex-col justify-center items-center relative overflow-visible px-2"
>
<cl-icon name="flight-takeoff-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("已发货") }}</cl-text
>
<cl-badge
type="primary"
:value="3"
position
:pt="{ className: '!right-6' }"
></cl-badge>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col justify-center items-center px-2">
<cl-icon name="exchange-cny-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("售后 / 退款") }}</cl-text
>
</view>
</cl-col>
</cl-row>
</view>
<cl-list :pt="{ className: 'mb-3' }">
<cl-list-item
:label="t('我的钱包')"
icon="wallet-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
<cl-list-item
:label="t('我的日报')"
icon="file-text-line"
arrow
hoverable
@tap="toDailyReport"
>
</cl-list-item>
<cl-list-item
:label="t('我的周报')"
icon="file-list-line"
arrow
hoverable
@tap="toWeeklyReport"
>
</cl-list-item>
<cl-list-item
:label="t('工作建议')"
icon="chat-voice-ai-line"
arrow
hoverable
@tap="toAdvice"
>
</cl-list-item>
<cl-list-item
:label="t('我的月报')"
icon="calendar-todo-line"
arrow
hoverable
@tap="toMonthlyReport"
>
</cl-list-item>
<cl-list-item
:label="t('数据看板')"
icon="pie-chart-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
<cl-list-item
:label="t('历史记录')"
icon="history-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
<cl-list-item
:label="t('邀请好友')"
icon="share-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
</cl-list>
<cl-list>
<cl-list-item :label="t('设置')" icon="settings-line" arrow hoverable @tap="toSet">
</cl-list-item>
</cl-list>
</view>
<!-- 自定义底部导航栏 -->
<custom-tabbar></custom-tabbar>
</cl-page>
</template>
<script setup lang="ts">
import { router, userInfo, useStore } from "@/cool";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import CustomTabbar from "@/components/tabbar.uvue";
const { user } = useStore();
const ui = useUi();
function toTest() {
ui.showToast({
message: t("开发中,敬请期待")
});
}
function toDailyReport() {
router.to("/pages/dailyreport/list");
}
function toWeeklyReport() {
router.to("/pages/weeklyreport/list");
}
function toAdvice() {
router.to('/pages/advice/index');
}
function toMonthlyReport() {
router.to("/pages/monthlyreport/list");
}
function toSet() {
router.to("/pages/set/index");
}
function toEdit() {
router.to("/pages/user/edit");
}
onReady(() => {
user.get();
});
</script>
<style lang="scss" scoped>
.top-icon {
@apply flex items-center justify-center rounded-lg bg-white mr-3 p-2;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<cl-page>
<view class="p-3">
<cl-list
v-for="(item, index) in list"
:key="index"
:title="item.label"
:pt="{
className: 'mb-5'
}"
>
<cl-list-item
v-for="child in item.children"
:key="child.label"
:label="child.label"
:arrow="child.path != null"
@tap="toPath(child)"
>
</cl-list-item>
</cl-list>
</view>
<!-- 自定义底部导航栏 -->
<custom-tabbar></custom-tabbar>
</cl-page>
</template>
<script setup lang="ts">
import CustomTabbar from "@/components/tabbar.uvue";
import { router } from "@/cool";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import { computed } from "vue";
const ui = useUi();
type Item = {
label: string;
path?: string;
children?: Item[];
};
const list = computed<Item[]>(() => [
{
label: t("社交"),
children: [
{
label: t("帖子详情"),
path: "/pages/template/post/detail"
}
]
},
{
label: t("商城"),
children: [
{
label: t("商品分类"),
path: "/pages/template/shop/goods-category"
},
{
label: t("商品详情"),
path: "/pages/template/shop/goods-detail/index"
},
{
label: t("商品列表、筛选")
},
{
label: t("购物车"),
path: "/pages/template/shop/shopping-cart"
},
{
label: t("订单列表、详情")
},
{
label: t("收货地址"),
path: "/pages/template/shop/address"
}
]
},
{
label: t("聊天"),
children: [
{
label: t("对话列表、历史记录")
}
]
},
{
label: "AI",
children: [
{
label: t("流式回复")
},
{
label: t("语言合成")
},
{
label: t("语音识别")
}
]
},
{
label: t("其他"),
children: [
{
label: t("文件管理")
}
]
}
]);
function toPath(item: Item) {
if (item.path == null) {
return ui.showToast({
message: t("该模板正在开发中")
});
}
router.to(item.path!);
}
</script>

View File

@@ -0,0 +1,93 @@
<template>
<cl-page :title="'\u6708\u62A5\u8BE6\u60C5'">
<view v-if="loading" class="flex justify-center items-center py-20">
<cl-loading />
</view>
<view v-else-if="report" class="p-4">
<!-- header -->
<view class="mb-6">
<view class="flex justify-between items-center mb-2">
<text class="text-2xl font-bold">{{ report.month || '-' }}</text>
</view>
<text class="text-sm text-gray-500">{{ report.submitTime ? ('\u63D0\u4EA4\u4E8E ' + formatDateTime(report.submitTime)) : (report.createTime ? ('\u521B\u5EFA\u4E8E ' + formatDateTime(report.createTime)) : '') }}</text>
</view>
<!-- content blocks -->
<view class="mb-6">
<text class="text-base font-bold mb-3 block">{{ '\u6708\u62A5\u5185\u5BB9' }}</text>
<view class="p-4 bg-white rounded-lg shadow-sm">
<text class="text-sm text-gray-800">{{ report.userEditedContent || report.aiFormattedContent || report.originalText || '\u6682\u65E0\u5185\u5BB9' }}</text>
</view>
</view>
<view v-if="report.originalText" class="mb-4">
<text class="text-base font-bold mb-2">{{ '\u539F\u59CB\u8F93\u5165\u5185\u5BB9' }}</text>
<view class="p-3 bg-gray-50 rounded">
<text class="text-sm text-gray-700">{{ report.originalText }}</text>
</view>
</view>
<view v-if="report.aiFormattedContent" class="mb-4">
<text class="text-base font-bold mb-2">{{ 'AI\u751F\u6210\u5185\u5BB9' }}</text>
<view class="p-3 bg-blue-50 rounded">
<text class="text-sm text-gray-800">{{ report.aiFormattedContent }}</text>
</view>
</view>
</view>
<view v-else class="flex flex-col items-center justify-center py-20">
<text class="text-6xl mb-4">{{ '\uD83D\uDCC5' }}</text>
<text class="text-gray-400 text-base">{{ '\u6708\u62A5\u4E0D\u5B58\u5728' }}</text>
<cl-button type="primary" size="small" class="mt-4" @tap="router.back()">{{ '\u8FD4\u56DE\u5217\u8868' }}</cl-button>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { request, router } from '@/cool';
import { useUi } from '@/uni_modules/cool-ui';
import { onLoad } from '@dcloudio/uni-app';
const ui = useUi();
const report = ref<any>(null);
const loading = ref(true);
const reportId = ref('');
onLoad(async (options: any) => {
reportId.value = (options?.id ?? '').toString();
if (!reportId.value) {
ui.showToast({ message: '\u53C2\u6570\u9519\u8BEF\uFF1A\u7F3A\u5C11id', type: 'error' });
setTimeout(() => router.back(), 700);
loading.value = false;
return;
}
await loadDetail();
});
async function loadDetail() {
loading.value = true;
try {
const res = await request({ url: '/app/monthlyreport/report/detail', method: 'GET', params: { id: reportId.value } });
report.value = res || null;
} catch (e: any) {
ui.showToast({ message: '\u52A0\u8F7D\u5931\u8D25: ' + (e.message || '\u672A\u77E5\u9519\u8BEF'), type: 'error' });
} finally {
loading.value = false;
}
}
function formatDateTime(v: string) {
if (!v) return '';
const d = new Date(v);
const Y = d.getFullYear();
const M = String(d.getMonth() + 1).padStart(2, '0');
const D = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
return `${Y}-${M}-${D} ${h}:${m}`;
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,152 @@
<template>
<cl-page title="我的月报">
<view class="p-4">
<!-- 筛选 -->
<view class="flex items-center gap-3 mb-4">
<cl-segmented
v-model="filterType"
:list="[
{ label: '全部', value: 'all' },
{ label: '草稿', value: 'draft' },
{ label: '已提交', value: 'submitted' }
]"
/>
<cl-button size="small" @tap="refresh">刷新</cl-button>
</view>
<!-- 列表 -->
<scroll-view scroll-y style="height: calc(100vh - 160px)" @scrolltolower="loadMore">
<view v-if="list.length > 0">
<view v-for="item in list" :key="item.id" class="p-4 mb-3 bg-white rounded-2xl" @tap="toDetail(item)">
<view class="flex items-center justify-between mb-1">
<text class="text-base font-bold">{{ item.month }}</text>
<view
:class="[
'px-2 py-1 rounded text-xs',
item.status === 1 ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
]"
>
{{ item.status === 1 ? "已提交" : "草稿" }}
</view>
</view>
<text class="text-gray-500 text-sm">{{ brief(item.userEditedContent || item.aiFormattedContent || item.originalText) }}</text>
<view class="text-xs text-gray-400 mt-2">
<text v-if="item.submitTime">提交于 {{ formatTime(item.submitTime) }}</text>
<text v-else-if="item.createTime">创建于 {{ formatTime(item.createTime) }}</text>
</view>
</view>
<view v-if="loading" class="flex items-center justify-center py-3">
<text class="text-gray-400">加载中...</text>
</view>
<view v-else-if="finished" class="flex items-center justify-center py-3">
<text class="text-gray-400">没有更多了</text>
</view>
</view>
<view v-else class="flex flex-col items-center justify-center py-20">
<text class="text-6xl mb-3">📅</text>
<text class="text-gray-400">暂无月报</text>
<view class="mt-4 px-4 py-2 bg-blue-500 text-white rounded" @tap="toSubmit">
<text>去提交月报</text>
</view>
</view>
</scroll-view>
<!-- 悬浮提交按钮 -->
<view class="fixed bottom-20 right-4">
<cl-button type="primary" size="large" round @tap="toSubmit">
<text class="text-2xl">✏️</text>
</cl-button>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { request, router, useStore } from '@/cool';
import { useUi } from '@/uni_modules/cool-ui';
const ui = useUi();
const { user } = useStore();
const list = ref<any[]>([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1);
const pageSize = ref(20);
const filterType = ref<'all' | 'draft' | 'submitted'>('all');
const userId = ref(0);
onMounted(async () => {
if (user.token) {
try { await user.get(); } catch {}
}
if (user.info.value?.id) {
userId.value = user.info.value.id;
loadReports();
} else {
ui.showToast({ message: '请先登录', type: 'error' });
setTimeout(() => router.to('/pages/user/login'), 1000);
}
});
function brief(s: string = '') {
const t = s.replace(/\n/g, ' ');
return t.length > 60 ? t.slice(0, 60) + '…' : t;
}
function statusClass(status: number) {
return status === 1 ? 'text-green-600' : 'text-orange-500';
}
function statusText(status: number) {
return status === 1 ? '已提交' : '草稿';
}
function formatTime(v: string) {
try {
const d = new Date(v);
const Y = d.getFullYear();
const M = String(d.getMonth() + 1).padStart(2, '0');
const D = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return `${Y}-${M}-${D} ${h}:${m}:${s}`;
} catch (e) { return v; }
}
async function loadReports() {
if (loading.value || finished.value) return;
loading.value = true;
try {
const params: any = { userId: userId.value, page: page.value, size: pageSize.value };
if (filterType.value === 'draft') params.status = 0;
if (filterType.value === 'submitted') params.status = 1;
const res = await request({ url: '/app/monthlyreport/report/myReports', method: 'GET', params });
if (res && res.length > 0) {
if (page.value === 1) list.value = res; else list.value.push(...res);
if (res.length < pageSize.value) finished.value = true;
page.value++;
} else {
if (page.value === 1) list.value = [];
finished.value = true;
}
} catch (e: any) {
ui.showToast({ message: e.message || '加载失败', type: 'error' });
} finally {
loading.value = false;
}
}
function loadMore() { loadReports(); }
function refresh() {
page.value = 1; finished.value = false; list.value = []; loadReports();
}
function toSubmit() { router.to('/pages/monthlyreport/submit'); }
function toDetail(item: any) { router.to(`/pages/monthlyreport/detail?id=${item.id}`); }
</script>
<style scoped></style>

View File

@@ -0,0 +1,173 @@
<template>
<cl-page title="提交月报">
<view class="p-4">
<!-- 月份选择 -->
<view class="mb-4">
<text class="text-base font-bold mb-2">月份</text>
<cl-input v-model="form.month" placeholder="例如2025-11" />
</view>
<!-- 原始内容输入 -->
<view class="mb-4">
<text class="text-base font-bold mb-2">工作内容(可选)</text>
<cl-input v-model="originalText" placeholder="请输入本月工作要点(可选)" :maxlength="4000" />
</view>
<!-- 从周报生成 -->
<view class="mb-4">
<cl-button type="primary" size="large" block @tap="generateFromWeekly" :loading="isGenerating">
{{ isGenerating ? '生成中...' : '🧩 从我的周报生成' }}
</cl-button>
</view>
<!-- AI格式化 -->
<view v-if="originalText" class="mb-4">
<cl-button type="success" size="large" block @tap="formatWithAI" :loading="isFormatting">
{{ isFormatting ? 'AI生成中...' : '🤖 AI格式化' }}
</cl-button>
</view>
<!-- AI结果与最终编辑 -->
<view v-if="aiFormattedContent" class="mb-4">
<text class="text-base font-bold mb-2">AI生成的月报</text>
<view class="p-3 bg-green-50 rounded-lg mb-2"><cl-markdown :content="aiFormattedContent" /></view>
<cl-input v-model="userEditedContent" placeholder="请编辑最终月报内容" :maxlength="6000" />
</view>
<!-- 操作按钮 -->
<view class="flex gap-3">
<cl-button type="default" class="flex-1" @tap="saveDraft" :loading="saving">保存草稿</cl-button>
<cl-button type="primary" class="flex-1" @tap="submitReport" :loading="submitting">提交</cl-button>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { request, useStore, router } from '@/cool';
import { useUi } from '@/uni_modules/cool-ui';
const ui = useUi();
const { user } = useStore();
const form = ref<any>({ month: '' });
const originalText = ref('');
const aiFormattedContent = ref('');
const userEditedContent = ref('');
const isFormatting = ref(false);
const isGenerating = ref(false);
const saving = ref(false);
const submitting = ref(false);
onMounted(async () => {
if (user.token) {
try { await user.get(); } catch {}
}
});
function ensureMonth(): string | null {
const v = (form.value.month || '').trim();
if (!/^\d{4}-\d{2}$/.test(v)) {
ui.showToast({ message: '请输入有效月份,如 2025-11', type: 'error' });
return null;
}
return v;
}
async function generateFromWeekly() {
const month = ensureMonth();
if (!month) return;
if (!user.info.value?.id) {
ui.showToast({ message: '请先登录', type: 'error' });
router.to('/pages/user/login');
return;
}
try {
isGenerating.value = true;
const res = await request({
url: '/app/monthlyreport/report/generateFromWeekly',
method: 'POST',
data: {
userId: user.info.value.id,
month,
templateKey: 'monthly_report_format'
}
});
aiFormattedContent.value = res.formattedContent || '';
userEditedContent.value = aiFormattedContent.value;
} catch (e: any) {
ui.showToast({ message: e.message || '生成失败', type: 'error' });
} finally {
isGenerating.value = false;
}
}
async function formatWithAI() {
const month = ensureMonth();
if (!month) return;
try {
isFormatting.value = true;
const res = await request({
url: '/app/monthlyreport/report/aiFormat',
method: 'POST',
data: { originalText: originalText.value, templateKey: 'monthly_report_format', month }
});
aiFormattedContent.value = res.formattedContent || '';
userEditedContent.value = aiFormattedContent.value;
} catch (e: any) {
ui.showToast({ message: e.message || 'AI格式化失败', type: 'error' });
} finally {
isFormatting.value = false;
}
}
async function saveDraft() {
const month = ensureMonth();
if (!month || !user.info.value?.id) return;
try {
saving.value = true;
await request({
url: '/app/monthlyreport/report/saveDraft',
method: 'POST',
data: {
userId: user.info.value.id,
month,
originalText: originalText.value,
aiFormattedContent: aiFormattedContent.value,
userEditedContent: userEditedContent.value,
inputType: 0
}
});
ui.showToast({ message: '已保存草稿', type: 'success' });
} catch (e: any) {
ui.showToast({ message: e.message || '保存失败', type: 'error' });
} finally { saving.value = false; }
}
async function submitReport() {
const month = ensureMonth();
if (!month || !user.info.value?.id) return;
try {
submitting.value = true;
await request({
url: '/app/monthlyreport/report/submit',
method: 'POST',
data: {
userId: user.info.value.id,
month,
originalText: originalText.value,
aiFormattedContent: aiFormattedContent.value,
userEditedContent: userEditedContent.value,
inputType: 0
}
});
ui.showToast({ message: '提交成功', type: 'success' });
router.back();
} catch (e: any) {
ui.showToast({ message: e.message || '提交失败', type: 'error' });
} finally { submitting.value = false; }
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,44 @@
<template>
<cl-page>
<view class="p-3">
<view class="flex flex-col items-center justify-center py-10">
<view class="p-3 bg-primary-500 rounded-xl">
<cl-image src="/static/logo.png" :height="80" :width="80"></cl-image>
</view>
<cl-text :pt="{ className: 'mt-3 mb-1' }">{{ config.name }}</cl-text>
<cl-text
color="info"
:pt="{
className: '!text-xs'
}"
>version {{ config.version }}</cl-text
>
</view>
<cl-list>
<cl-list-item
:label="t('访问官网')"
arrow
hoverable
@tap="toWebSite"
></cl-list-item>
</cl-list>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { config } from "@/config";
import { $t, t } from "@/locale";
import { openWeb } from "@/uni_modules/cool-open-web";
onReady(() => {
uni.setNavigationBarTitle({
title: $t("关于{name}", { name: config.name })
});
});
function toWebSite() {
openWeb(config.website);
}
</script>

View File

@@ -0,0 +1,39 @@
<template>
<cl-page>
<view class="p-10 flex flex-col items-center justify-center">
<cl-text :pt="{ className: 'text-center mb-5' }"
>我们期待与您携手,共同探索技术的边界,共同推动技术的进步</cl-text
>
<view class="p-2 bg-white mb-5 rounded-xl">
<cl-image
src="/static/cs.png"
:height="320"
:width="320"
show-menu-by-longpress
></cl-image>
</view>
<!-- #ifndef H5 -->
<cl-button type="light" icon="download-line" @tap="saveImage">保存图片</cl-button>
<!-- #endif -->
</view>
</cl-page>
</template>
<script setup lang="ts">
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
function saveImage() {
uni.saveImageToPhotosAlbum({
filePath: "/static/cs.png",
success: () => {
ui.showToast({
message: "保存成功"
});
}
});
}
</script>

View File

@@ -0,0 +1,42 @@
<template>
<cl-page>
<view class="p-3">
<cl-list>
<cl-list-item :label="t('深色模式')">
<cl-switch :model-value="isDark" @change="onThemeChange"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('多语言')" arrow hoverable @tap="setLocale"> </cl-list-item>
<cl-list-item :label="t('字体大小')" arrow hoverable @tap="setSize"> </cl-list-item>
</cl-list>
</view>
<!-- 语言设置 -->
<locale-set :ref="refs.set('localeSet')"></locale-set>
<!-- 字体大小设置 -->
<size-set :ref="refs.set('sizeSet')"></size-set>
</cl-page>
</template>
<script setup lang="ts">
import { isDark, toggleTheme, useRefs } from "@/cool";
import { t } from "@/locale";
import LocaleSet from "@/components/locale-set.uvue";
import SizeSet from "@/components/size-set.uvue";
const refs = useRefs();
function onThemeChange() {
toggleTheme();
}
function setLocale() {
refs.open("localeSet");
}
function setSize() {
refs.open("sizeSet");
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<cl-page>
<view class="p-3">
<cl-list :pt="{ className: 'mb-3' }">
<cl-list-item
:label="t('通用设置')"
icon="settings-line"
arrow
hoverable
@tap="router.to('/pages/set/general')"
>
</cl-list-item>
<cl-list-item
:label="t('通知设置')"
icon="notification-4-line"
arrow
hoverable
@tap="router.to('/pages/set/notice')"
>
</cl-list-item>
<cl-list-item :label="t('隐私设置')" icon="lock-line" arrow hoverable>
</cl-list-item>
</cl-list>
<cl-list :pt="{ className: 'mb-3' }">
<cl-list-item
:label="$t('关于{name}', { name: config.name })"
icon="error-warning-line"
arrow
hoverable
:pt="{
label: {
className: 'flex-1'
}
}"
@tap="router.to('/pages/set/about')"
>
</cl-list-item>
<cl-list-item
:label="t('联系客服')"
icon="customer-service-line"
arrow
hoverable
@tap="router.to('/pages/set/cs')"
>
</cl-list-item>
</cl-list>
<cl-list :pt="{ className: 'mb-3' }">
<cl-list-item hoverable justify="center" @tap="toLogout">
<cl-text color="error">{{ t("退出登录") }}</cl-text>
</cl-list-item>
</cl-list>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { config } from "@/config";
import { router, useStore } from "@/cool";
import { $t, t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const { user } = useStore();
function toLogout() {
ui.showConfirm({
title: t("提示"),
message: t("确定退出登录吗?"),
callback(action) {
if (action == "confirm") {
user.logout();
}
}
});
}
</script>

View File

@@ -0,0 +1,15 @@
<template>
<cl-page>
<view class="p-3">
<cl-list>
<cl-list-item :label="t('开启通知')">
<cl-switch></cl-switch>
</cl-list-item>
</cl-list>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { t } from "@/locale";
</script>

View File

@@ -0,0 +1,399 @@
<template>
<cl-page>
<cl-sticky>
<cl-topbar fixed safe-area-top :height="isMp() ? null : 100">
<view class="flex flex-row items-center w-full pl-[72rpx] pr-3">
<cl-avatar
rounded
:size="60"
src="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-avatar>
<cl-text :pt="{ className: 'mx-3' }">神仙都没用</cl-text>
<cl-button
text
border
size="small"
rounded
:pt="{ className: parseClass(['!px-3', [!isMp(), 'ml-auto']]) }"
>立即关注</cl-button
>
</view>
</cl-topbar>
</cl-sticky>
<view class="banner">
<cl-banner
height="1000rpx"
:list="bannerList"
:pt="{
className: '!rounded-none',
image: {
className: '!rounded-none'
}
}"
></cl-banner>
</view>
<view class="bg-white dark:bg-surface-800 mb-3">
<view
class="p-4 border border-solid border-b-surface-200 dark:border-b-surface-600 border-t-0 border-l-0 border-r-0"
>
<cl-text bold :pt="{ className: 'mb-2 text-lg' }">❄️ 雪地旅行商务团价格表</cl-text>
<cl-text :pt="{ className: 'mb-3' }">
对于不想做攻略的小伙伴来说定制团真的是最佳选择。雪地自驾没有经验的真心不建议。费用清晰明了。预算很准确,收费很合理。</cl-text
>
<view class="flex-row mb-4">
<view
class="flex-row items-center border border-solid border-surface-300 dark:border-surface-600 rounded-lg px-3 py-1 pl-2"
>
<cl-icon name="map-pin-2-line" :size="30"></cl-icon>
<cl-text :pt="{ className: 'text-sm ml-1' }"
>哈尔滨 · 圣 · 索菲亚教堂</cl-text
>
</view>
</view>
<cl-text :pt="{ className: 'text-sm' }" color="info">编辑于 2天前</cl-text>
</view>
<!-- 评论 -->
<view class="p-4">
<cl-text :pt="{ className: 'mb-4 text-sm' }">共 10 条评论</cl-text>
<!-- 评论输入框 -->
<view class="flex-row reply items-center mb-5" @tap="openReply()">
<cl-avatar
:size="68"
rounded
:pt="{ className: 'mr-2' }"
src="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-avatar>
<view
class="h-[69rpx] flex-1 flex-row items-center bg-surface-100 dark:bg-surface-700 rounded-full px-4"
>
<cl-text color="info" :pt="{ className: 'text-sm flex-1' }"
>说点什么...</cl-text
>
<cl-icon name="user-smile-line" :size="40"></cl-icon>
<cl-icon
name="image-circle-line"
:pt="{ className: 'ml-2' }"
:size="40"
></cl-icon>
</view>
</view>
<view class="list">
<view class="flex-row mb-6" v-for="item in commentList" :key="item.id">
<cl-avatar :size="68" rounded :src="item.avatar"></cl-avatar>
<view class="ml-3 flex-1">
<!-- 评论者信息 -->
<view class="flex-row items-center mb-1">
<cl-text :pt="{ className: 'text-sm' }" color="info">{{
item.name
}}</cl-text>
<cl-tag
plain
rounded
:pt="{
className: '!px-1 !py-[2rpx] ml-2',
text: { className: '!text-xs' }
}"
v-if="item.isAuthor"
>作者</cl-tag
>
</view>
<!-- 评论内容 -->
<cl-text :pt="{ className: 'mb-1' }">{{ item.content }}</cl-text>
<view class="flex-row items-center">
<!-- 评论时间 -->
<cl-text :pt="{ className: 'text-sm' }" color="info">
{{ item.time }}
</cl-text>
<cl-text
color="info"
:pt="{ className: 'text-sm ml-3' }"
@tap="openReply()"
>回复</cl-text
>
</view>
<!-- 回复列表 -->
<view class="flex-row mt-3" v-for="reply in item.reply" :key="reply.id">
<cl-avatar :size="50" rounded :src="reply.avatar"> </cl-avatar>
<view class="ml-3 flex-1">
<view class="flex-row items-center mb-1">
<cl-text :pt="{ className: 'text-sm' }" color="info">{{
reply.name
}}</cl-text>
<cl-tag
plain
rounded
:pt="{
className: '!px-1 !py-[2rpx] ml-2',
text: { className: '!text-xs' }
}"
v-if="reply.isAuthor"
>作者</cl-tag
>
</view>
<cl-text :pt="{ className: 'mb-1' }">{{
reply.content
}}</cl-text>
<view class="flex-row items-center mb-1">
<cl-text :pt="{ className: 'text-sm' }" color="info">
{{ reply.time }}
</cl-text>
<cl-text
color="info"
:pt="{ className: 'text-sm ml-3' }"
@tap="openReply()"
>回复</cl-text
>
</view>
<cl-text color="info" :pt="{ className: 'text-sm' }"
>展开 1 条回复</cl-text
>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<cl-footer>
<view class="flex flex-row h-[68rpx]">
<view
class="flex-row items-center bg-surface-100 dark:bg-surface-700 rounded-full px-3 py-2 mr-6 w-[260rpx] h-full"
@tap="openReply"
>
<cl-icon name="edit-line" color="info" :size="32"></cl-icon>
<cl-text color="info" :pt="{ className: 'text-sm ml-2' }">说点什么...</cl-text>
</view>
<view class="flex flex-row flex-1 justify-end h-full items-center">
<view class="flex-row justify-center items-center mr-6">
<cl-icon name="heart-line" :size="40"></cl-icon>
<cl-text :pt="{ className: 'ml-2 text-sm' }">700</cl-text>
</view>
<view class="flex-row justify-center items-center">
<cl-icon name="chat-3-line" :size="40"></cl-icon>
<cl-text :pt="{ className: 'ml-2 text-sm' }">59</cl-text>
</view>
</view>
</view>
</cl-footer>
<!-- 回复弹窗 -->
<cl-popup v-model="replyVisible" direction="bottom" ref="replyPopupRef">
<view class="p-4 pt-0">
<view class="bg-surface-100 dark:bg-surface-800 rounded-2xl p-[8rpx] h-[168rpx]">
<cl-textarea
placeholder="说点什么..."
:border="false"
:pt="{
className: parseClass(['!bg-transparent'])
}"
autofocus
v-if="isApp() ? true : replyPopupRef?.isOpened == true"
></cl-textarea>
</view>
<view class="flex-row items-center mt-3 pl-2">
<cl-icon
name="image-circle-line"
:pt="{ className: 'mr-4' }"
:size="44"
></cl-icon>
<cl-icon
name="user-smile-line"
:pt="{ className: 'mr-4' }"
:size="44"
></cl-icon>
<cl-button :pt="{ className: 'ml-auto !px-4' }" rounded>发送</cl-button>
</view>
</view>
</cl-popup>
</cl-page>
</template>
<script lang="ts" setup>
import { isApp, isMp, parseClass } from "@/cool";
import { ref } from "vue";
const bannerList = ref<string[]>([
"https://unix.cool-js.com/images/demo/bg1.png",
"https://unix.cool-js.com/images/demo/bg2.png",
"https://unix.cool-js.com/images/demo/bg3.png"
]);
type Comment = {
id: number;
avatar: string;
name: string;
content: string;
time: string;
isAuthor: boolean;
order: number;
reply: Comment[];
};
const commentList = ref<Comment[]>([
{
id: 1,
avatar: "https://unix.cool-js.com/images/demo/avatar-1.jpg",
name: "李明",
content: "导游讲解很专业,风景绝美,是一次难忘的旅行体验!",
time: "2024-06-20 10:15",
isAuthor: false,
order: 1,
reply: []
},
{
id: 2,
avatar: "https://unix.cool-js.com/images/demo/avatar-2.jpg",
name: "小芳",
content: "酒店干净卫生,位置也很方便,强烈推荐给大家!",
time: "2024-06-20 11:08",
isAuthor: false,
order: 2,
reply: [
{
id: 201,
avatar: "https://unix.cool-js.com/images/demo/avatar.jpg",
name: "神仙都没用",
content: "感谢您的推荐,欢迎再次光临哦~",
time: "2024-06-20 12:00",
isAuthor: true,
order: 1,
reply: []
}
]
},
{
id: 3,
avatar: "https://unix.cool-js.com/images/demo/avatar-3.jpg",
name: "王科",
content: "行程安排合理,吃住都很满意,导游态度超级好。",
time: "2024-06-19 09:22",
isAuthor: false,
order: 3,
reply: []
},
{
id: 4,
avatar: "https://unix.cool-js.com/images/demo/avatar-4.jpg",
name: "艳艳",
content: "第一次带父母出游,家人都开心,下次还会选择这里~",
time: "2024-06-18 15:40",
isAuthor: false,
order: 4,
reply: []
},
{
id: 5,
avatar: "https://unix.cool-js.com/images/demo/avatar-5.jpg",
name: "孙强",
content: "车程舒适,时间安排紧凑但不赶,非常不错,非常棒!",
time: "2024-06-17 17:05",
isAuthor: false,
order: 5,
reply: [
{
id: 501,
avatar: "https://unix.cool-js.com/images/demo/avatar.jpg",
name: "神仙都没用",
content: "您的好评就是我们最大的动力,谢谢支持~",
time: "2024-06-17 17:45",
isAuthor: true,
order: 1,
reply: []
}
]
},
{
id: 6,
avatar: "https://unix.cool-js.com/images/demo/avatar-6.jpg",
name: "导游小陈",
content: "很高兴大家玩的开心,有问题随时联系我们。",
time: "2024-06-17 18:20",
isAuthor: false,
order: 6,
reply: []
},
{
id: 7,
avatar: "https://unix.cool-js.com/images/demo/avatar-7.jpg",
name: "Grace",
content: "风景如画,照片拍了一堆,朋友都夸赞说好美!",
time: "2024-06-16 14:10",
isAuthor: false,
order: 7,
reply: []
},
{
id: 8,
avatar: "https://unix.cool-js.com/images/demo/avatar-8.jpg",
name: "阿伟",
content: "活动内容丰富,小朋友特别喜欢,下次会再来~",
time: "2024-06-15 19:28",
isAuthor: false,
order: 8,
reply: []
},
{
id: 9,
avatar: "https://unix.cool-js.com/images/demo/avatar-9.jpg",
name: "小虎",
content: "天气晴朗,安排贴心,吃到了很多特色美食,值了!",
time: "2024-06-14 13:52",
isAuthor: false,
order: 9,
reply: []
},
{
id: 10,
avatar: "https://unix.cool-js.com/images/demo/avatar-10.jpg",
name: "Julia",
content: "贴心的讲解和服务,体验很棒,感谢本次旅行团队!",
time: "2024-06-13 16:34",
isAuthor: false,
order: 10,
reply: []
}
]);
const replyPopupRef = ref<ClPopupComponentPublicInstance | null>(null);
const replyVisible = ref(false);
function openReply() {
replyVisible.value = true;
}
function closeReply() {
replyVisible.value = false;
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,199 @@
<template>
<cl-page>
<view class="p-3">
<view class="p-4 bg-white rounded-2xl dark:!bg-surface-800 mb-3">
<cl-form ref="formRef" v-model="formData" :rules="rules" :disabled="saving">
<cl-form-item :label="t('收货人')" prop="contact" required>
<cl-input
v-model="formData.contact"
:placeholder="t('请输入收货人姓名')"
></cl-input>
</cl-form-item>
<cl-form-item :label="t('手机号')" prop="phone" required>
<cl-input
v-model="formData.phone"
:placeholder="t('请输入手机号')"
:maxlength="11"
type="number"
></cl-input>
</cl-form-item>
<cl-form-item :label="t('地区')" prop="province" required>
<cl-cascader
v-model="regions"
:placeholder="t('选择省市区')"
:options="pcaOptions"
@change="onRegionsChange"
></cl-cascader>
</cl-form-item>
<cl-form-item :label="t('详细地址')" prop="address" required>
<cl-input
v-model="formData.address"
:placeholder="t('小区楼栋、门牌号、村等')"
></cl-input>
</cl-form-item>
</cl-form>
</view>
<cl-list>
<cl-list-item :label="t('默认地址')">
<cl-switch v-model="formData.isDefault"></cl-switch>
</cl-list-item>
</cl-list>
</view>
<cl-footer>
<cl-button @tap="save()">{{ t("保存") }}</cl-button>
</cl-footer>
</cl-page>
</template>
<script lang="ts" setup>
import { router, isEmpty, type Response, request, parse } from "@/cool";
import { t } from "@/locale";
import { useCascader, useForm, useUi, type ClFormRule } from "@/uni_modules/cool-ui";
import { type Ref, ref } from "vue";
import pca from "@/data/pca.json";
import type { UserAddress } from "../types";
const props = defineProps({
id: {
type: String,
default: ""
}
});
const ui = useUi();
const { formRef, validate } = useForm();
// 省市区级联选项数据
const pcaOptions = useCascader(pca);
// 地区选择的值,格式为 [省, 市, 区]
const regions = ref<string[]>([]);
// 表单数据,包含收货人、手机号、地区、详细地址、是否默认等字段
const formData = ref<UserAddress>({
contact: "",
phone: "",
province: "",
city: "",
district: "",
address: "",
isDefault: false
}) as Ref<UserAddress>;
// 表单验证规则,校验收货人、手机号、详细地址、地区等必填项
const rules = new Map<string, ClFormRule[]>([
["contact", [{ required: true, message: t("收货人不能为空") }]],
[
"phone",
[
{ required: true, message: t("手机号不能为空") },
{ pattern: /^1[3-9]\d{9}$/, message: t("手机号格式不正确") }
]
],
["address", [{ required: true, message: t("详细地址不能为空") }]],
["province", [{ required: true, message: t("所在地区不能为空") }]]
]);
// 保存按钮loading状态
const saving = ref(false);
/**
* 保存地址信息
* 1. 校验表单
* 2. 组装数据
* 3. 请求后端接口,新增或更新地址
*/
function save() {
validate((valid, errors) => {
if (valid) {
ui.showLoading(t("保存中"));
// 解构地区信息
const [province, city, district] = regions.value;
saving.value = true;
// 合并表单数据和地区信息
const data = {
...formData.value,
province,
city,
district
};
// 根据是否有id判断是新增还是编辑
request({
url: `/app/user/address/${props.id != "" ? "update" : "add"}`,
method: "POST",
data
})
.then(() => {
// 保存成功返回上一页
router.back();
})
.catch((err) => {
// 保存失败提示错误信息
ui.showToast({ message: (err as Response).message! });
})
.finally(() => {
ui.hideLoading();
saving.value = false;
});
} else {
// 校验失败提示第一个错误
ui.showToast({ message: errors[0].message });
}
});
}
/**
* 获取地址详情(编辑时调用)
* 1. 请求后端接口获取地址详情
* 2. 回填表单和地区选择
*/
function getInfo() {
request({
url: "/app/user/address/info",
data: { id: props.id }
})
.then((res) => {
if (res != null) {
// 解析并赋值表单数据
formData.value = parse<UserAddress>(res)!;
// 回填地区选择
regions.value = [
formData.value.province,
formData.value.city,
formData.value.district
];
}
})
.catch((err) => {
ui.showToast({ message: (err as Response).message! });
});
}
/**
* 地区选择变化时触发
* @param value 选中的地区数组 [省, 市, 区]
*/
function onRegionsChange(value: string[]) {
const [province, city, district] = isEmpty(value) ? ["", "", ""] : value;
formData.value.province = province;
formData.value.city = city;
formData.value.district = district;
}
onReady(() => {
if (props.id != "") {
getInfo();
}
});
</script>

View File

@@ -0,0 +1,221 @@
<template>
<cl-page>
<view class="p-3">
<view
class="flex flex-col bg-white rounded-2xl p-4 mb-3 dark:!bg-surface-800"
:class="{
'!mb-0': index == addressList.length - 1
}"
v-for="(item, index) in addressList"
:key="item.id"
>
<view class="flex flex-col">
<cl-text color="info" :pt="{ className: '!text-sm' }"
>{{ item.province }} {{ item.city }} {{ item.district }}</cl-text
>
<cl-text
:pt="{
className: 'my-1'
}"
>{{ item.address }}</cl-text
>
<view class="flex flex-row">
<cl-text :pt="{ className: '!text-sm' }">{{ item.contact }}</cl-text>
<cl-text color="info" :pt="{ className: 'ml-3 !text-sm' }">{{
item.phone
}}</cl-text>
</view>
</view>
<view
class="flex flex-row border border-solid border-gray-100 border-b-0 border-l-0 border-r-0 pt-4 mt-4 dark:!border-surface-700"
>
<cl-radio
v-model="defaultId"
active-icon="checkbox-circle-fill"
inactive-icon="checkbox-blank-circle-line"
:pt="{
className: 'max-w-[300rpx]',
label: {
className: '!text-sm'
},
icon: {
size: 32
}
}"
:value="item.id"
@change="onDefaultChange(item)"
>{{ item.isDefault ? t("已设为默认") : t("设为默认") }}</cl-radio
>
<view
class="flex flex-row items-center justify-center ml-auto"
@tap="onDelete(item.id!)"
>
<cl-icon name="delete-bin-line" :size="28"></cl-icon>
<cl-text :pt="{ className: 'ml-2 !text-sm' }">{{ t("删除") }}</cl-text>
</view>
<view
class="flex flex-row items-center justify-center ml-6"
@tap="toEdit(item.id!)"
>
<cl-icon name="edit-line" :size="28"></cl-icon>
<cl-text :pt="{ className: 'ml-2 !text-sm' }">{{ t("修改") }}</cl-text>
</view>
</view>
</view>
<cl-empty v-if="list.length == 0"></cl-empty>
</view>
<cl-footer>
<cl-button @tap="toAdd()">{{ t("添加地址") }}</cl-button>
</cl-footer>
</cl-page>
</template>
<script lang="ts" setup>
import { useUi } from "@/uni_modules/cool-ui";
import { parse, request, router, usePager, type Response } from "@/cool";
import { t } from "@/locale";
import { computed, ref } from "vue";
import type { UserAddress } from "../types";
const ui = useUi();
const { refresh, list, loadMore } = usePager(async (data, { render }) => {
await request({
url: "/app/user/address/page",
method: "POST",
data
})
.then((res) => {
if (res != null) {
render(res);
}
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
})
.finally(() => {
ui.hideLoading();
});
});
// 默认地址id
const defaultId = ref<number>(0);
// 地址列表数据
const addressList = computed(() =>
list.value.map((e) => {
e["isDefault"] = e["isDefault"] == 1 ? true : false;
const d = parse<UserAddress>(e)!;
if (d.isDefault) {
defaultId.value = d.id!;
}
return d;
})
);
// 添加地址
function toAdd() {
router.to("/pages/template/shop/address-edit");
}
// 编辑地址
function toEdit(id: number) {
router.push({
path: "/pages/template/shop/address-edit",
query: { id }
});
}
// 删除地址
function onDelete(id: number) {
ui.showConfirm({
title: t("提示"),
message: t("删除地址后无法恢复,确认要删除该地址吗?"),
callback: (action) => {
if (action == "confirm") {
request({
url: "/app/user/address/delete",
method: "POST",
data: { ids: [id] }
})
.then(() => {
ui.showToast({
message: t("删除成功")
});
refresh({});
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
});
}
}
});
}
// 设为默认地址
function onDefaultChange(item: UserAddress) {
// 遍历地址列表,设置选中的地址为默认地址,其他地址取消默认
addressList.value.forEach((e) => {
if (e.id == item.id) {
// 切换当前地址的默认状态
e.isDefault = !e.isDefault;
// 如果取消了默认则重置默认地址ID
if (!e.isDefault) {
defaultId.value = 0;
}
} else {
// 其他地址全部取消默认
e.isDefault = false;
}
});
request({
url: "/app/user/address/update",
method: "POST",
data: {
id: item.id,
isDefault: item.isDefault
}
});
}
onPullDownRefresh(() => {
refresh({ page: 1 }).finally(() => {
uni.stopPullDownRefresh();
});
});
onReachBottom(() => {
loadMore();
});
onReady(() => {
ui.showLoading(t("加载中"));
// 默认请求
refresh({
page: 1,
size: 20
});
onPageShow(() => {
refresh({});
});
});
</script>

View File

@@ -0,0 +1,335 @@
<template>
<cl-page>
<view class="flex flex-col h-full">
<view class="flex flex-row p-3">
<cl-input
:pt="{
className: parseClass(['flex-1 !border-2 !rounded-xl'])
}"
prefix-icon="search-line"
placeholder="iPhone 16 Pro Max"
></cl-input>
</view>
<view class="flex flex-row flex-1">
<!-- 左侧分类列表 -->
<view class="h-full w-[200rpx] bg-white dark:bg-surface-800 mr-2 rounded-tr-xl">
<scroll-view direction="vertical" :show-scrollbar="false" class="h-full">
<view
class="h-[100rpx] p-2"
v-for="(item, index) in list"
:key="item.label"
@click="onCategoryChange(index)"
>
<view
class="flex flex-row items-center justify-center h-full rounded-lg"
:class="[
categoryActive == index
? isDark
? 'bg-primary-500'
: 'bg-primary-50'
: ''
]"
>
<cl-text
:pt="{
className: parseClass([
[
categoryActive == index,
isDark ? '!text-white' : '!text-primary-500',
isDark ? '!text-surface-300' : '!text-surface-500'
]
])
}"
>{{ item.label }}</cl-text
>
</view>
</view>
</scroll-view>
</view>
<!-- 右侧商品列表 -->
<view class="flex-1">
<scroll-view
direction="vertical"
:scroll-into-view="scrollIntoView"
:scroll-with-animation="!scrollLock"
class="h-full"
@scroll="onScroll"
>
<view class="pr-2">
<view
class="category-item flex rounded-xl bg-white dark:bg-surface-800 mb-2 pb-3"
v-for="(item, index) in list"
:key="item.label"
:id="`category-item-${index}`"
>
<cl-text
:pt="{
className: 'p-3'
}"
>{{ item.label }}</cl-text
>
<view class="px-1">
<cl-row :gutter="10">
<cl-col
v-for="goods in item.list"
:key="goods.name"
:span="8"
>
<view class="flex items-center flex-col justify-center">
<cl-image :src="goods.image"></cl-image>
<cl-text
:ellipsis="true"
:pt="{ className: '!text-xs text-center mt-2' }"
>{{ goods.name }}</cl-text
>
</view>
</cl-col>
</cl-row>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { isDark, parseClass } from "@/cool";
import { getCurrentInstance, ref } from "vue";
const { proxy } = getCurrentInstance()!;
// 商品类型
type Goods = {
name: string;
price: number;
image: string;
};
// 分类类型
type Category = {
label: string;
top?: number;
list: Goods[];
};
// 商品分类示例数据
const list = ref<Category[]>([
{
label: "推荐",
list: [
{
name: "iPhone 15 Pro",
price: 8999,
image: "/static/goods/iphone15pro.png"
},
{
name: "华为 Mate 60 Pro",
price: 6999,
image: "/static/goods/mate60pro.png"
},
{
name: "小米 14 Ultra",
price: 5999,
image: "/static/goods/xiaomi14ultra.png"
}
]
},
{
label: "Apple",
list: [
{
name: "iPhone 15 Pro",
price: 8999,
image: "/static/goods/iphone15pro.png"
},
{
name: "iPhone 14",
price: 5999,
image: "/static/goods/iphone14.png"
}
]
},
{
label: "华为",
list: [
{
name: "华为 Mate 60 Pro",
price: 6999,
image: "/static/goods/mate60pro.png"
},
{
name: "华为 P60",
price: 4999,
image: "/static/goods/p60.png"
}
]
},
{
label: "小米",
list: [
{
name: "小米 14 Ultra",
price: 5999,
image: "/static/goods/xiaomi14ultra.png"
},
{
name: "小米 13",
price: 3999,
image: "/static/goods/xiaomi13.png"
}
]
},
{
label: "三星",
list: [
{
name: "三星 Galaxy S24",
price: 7999,
image: "/static/goods/galaxys24.png"
},
{
name: "三星 Galaxy Z Flip5",
price: 8999,
image: "/static/goods/galaxyzflip5.png"
}
]
},
{
label: "OPPO",
list: [
{
name: "OPPO Find X7",
price: 4999,
image: "/static/goods/findx7.png"
}
]
},
{
label: "VIVO",
list: [
{
name: "VIVO X100 Pro",
price: 5999,
image: "/static/goods/x100pro.png"
}
]
},
{
label: "荣耀",
list: [
{
name: "荣耀 Magic6",
price: 4599,
image: "/static/goods/magic6.png"
}
]
},
{
label: "一加",
list: [
{
name: "一加 12",
price: 4299,
image: "/static/goods/oneplus12.png"
}
]
},
{
label: "红米",
list: [
{
name: "红米 K70 Pro",
price: 3299,
image: "/static/goods/k70pro.png"
}
]
},
{
label: "魅族",
list: [
{
name: "魅族 21",
price: 3999,
image: "/static/goods/meizu21.png"
}
]
},
{
label: "坚果",
list: [
{
name: "坚果 R2",
price: 2999,
image: "/static/goods/nutR2.png"
}
]
},
{
label: "其他",
list: [
{
name: "诺基亚 X30",
price: 2599,
image: "/static/goods/nokiax30.png"
}
]
}
]);
// 滚动到指定分类
const scrollIntoView = ref("");
// 滚动锁定
const scrollLock = ref(false);
// 当前选中的分类
const categoryActive = ref(0);
// 分类切换
function onCategoryChange(index: number) {
categoryActive.value = index;
scrollIntoView.value = `category-item-${index}`;
scrollLock.value = true;
setTimeout(() => {
scrollLock.value = false;
}, 350);
}
// 滚动时,更新当前选中的分类
function onScroll(e: UniScrollEvent) {
if (scrollLock.value) return;
const top = e.detail.scrollTop;
list.value.forEach((e, i) => {
if (top >= e.top!) {
categoryActive.value = i;
}
});
}
// 初始化
function init() {
uni.createSelectorQuery()
.in(proxy!.$root)
.selectAll(".category-item")
.boundingClientRect((res) => {
(res as NodeInfo[]).forEach((e, i, arr) => {
list.value[i].top = (e.top ?? 0) - (arr[0].top ?? 0);
});
})
.exec();
}
onReady(() => {
init();
});
</script>

View File

@@ -0,0 +1,81 @@
<template>
<view class="flex">
<view class="flex flex-row items-center mb-3">
<cl-text>买家评论 78</cl-text>
<cl-icon name="arrow-right-s-line" :pt="{ className: 'ml-auto' }"></cl-icon>
</view>
<view class="flex flex-col">
<view class="flex flex-row my-3" v-for="item in list" :key="item.id">
<cl-avatar :size="72" rounded :src="item.avatar"></cl-avatar>
<view class="flex-1 ml-4">
<view class="flex flex-row items-center justify-between">
<cl-text>{{ item.name }}</cl-text>
<cl-text color="info" :pt="{ className: 'text-xs' }">{{
item.time
}}</cl-text>
</view>
<cl-text ellipsis :lines="2" :pt="{ className: 'mt-1 text-sm' }">{{
item.content
}}</cl-text>
</view>
<view class="ml-3 relative">
<cl-image
:height="100"
:width="100"
:pt="{
inner: {
className: '!rounded-lg'
}
}"
src="https://unix.cool-js.com/images/demo/bg1.png"
/>
<view class="bg-black/60 rounded-full px-1 absolute top-9 right-1">
<text class="text-xs text-white">+3</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
type Comment = {
id: number;
avatar: string;
name: string;
content: string;
time: string;
};
const list = ref<Comment[]>([
{
id: 1,
avatar: "https://unix.cool-js.com/images/demo/avatar-1.jpg",
name: "李明",
content: "导游讲解很专业,风景绝美,是一次难忘的旅行体验!",
time: "几分钟前"
},
{
id: 2,
avatar: "https://unix.cool-js.com/images/demo/avatar-2.jpg",
name: "小芳",
content: "酒店干净卫生,位置也很方便,强烈推荐给大家!",
time: "1小时前"
},
{
id: 3,
avatar: "https://unix.cool-js.com/images/demo/avatar-3.jpg",
name: "王科",
content: "行程安排合理,吃住都很满意,导游态度超级好。",
time: "5天前"
}
]);
</script>

View File

@@ -0,0 +1,15 @@
<template>
<view class="flex">
<image v-for="item in list" :key="item" :src="item" mode="widthFix" class="w-full" />
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
const list = ref<string[]>([
"https://unix.cool-js.com/images/demo/goods/desc-1.jpg",
"https://unix.cool-js.com/images/demo/goods/desc-2.jpg",
"https://unix.cool-js.com/images/demo/goods/desc-3.jpg"
]);
</script>

View File

@@ -0,0 +1,116 @@
<template>
<cl-page>
<Topbar />
<cl-banner
:list="bannerList"
:height="800"
:pt="{
className: '!rounded-none',
image: {
className: '!rounded-none'
}
}"
/>
<view class="card dark:!bg-surface-700" id="info">
<Info />
</view>
<view class="card !py-1 dark:!bg-surface-700">
<view class="row is-border dark:!border-surface-600">
<cl-icon name="apps-line"></cl-icon>
<view class="flex-1 ml-3">
<cl-text>已选:黑色 128GB + 碎屏险</cl-text>
</view>
<cl-icon name="arrow-right-s-line"></cl-icon>
</view>
<view class="row is-border !items-start dark:!border-surface-600">
<cl-icon name="truck-line" :pt="{ className: '!mt-1' }"></cl-icon>
<view class="flex-1 ml-3">
<cl-text color="info">预计11月1日 周三 送达</cl-text>
<cl-text color="info" :pt="{ className: '!mt-1' }">深圳益田假日广场</cl-text>
</view>
</view>
<view class="row">
<cl-icon name="shield-check-line"></cl-icon>
<view class="flex-1 ml-3">
<cl-text color="info">7天无理由退货 · 正品保证 · 极速发货 · 无忧售后</cl-text>
</view>
</view>
</view>
<view class="card dark:!bg-surface-700" id="comment">
<Comment />
</view>
<view class="card !p-0 dark:!bg-surface-700" id="desc">
<Desc />
</view>
<cl-footer>
<view class="flex flex-row overflow-visible">
<view class="flex flex-row mr-auto overflow-visible">
<view class="flex justify-center items-center px-4">
<cl-icon name="heart-line" :size="42"></cl-icon>
<cl-text :pt="{ className: 'text-xs mt-1' }">收藏</cl-text>
</view>
<view
class="flex justify-center items-center px-4 overflow-visible"
@tap="router.to('/pages/template/shop/shopping-cart')"
>
<cl-icon name="shopping-cart-line" :size="42"></cl-icon>
<cl-text :pt="{ className: 'text-xs mt-1' }">购物车</cl-text>
<cl-badge
type="error"
:value="3"
position
:pt="{ className: '!right-[24rpx] !top-[-10rpx] !scale-80' }"
></cl-badge>
</view>
</view>
<cl-button text border :pt="{ className: '!w-[220rpx]' }">加入购物车</cl-button>
<cl-button :pt="{ className: '!w-[220rpx]' }">立即购买</cl-button>
</view>
</cl-footer>
</cl-page>
</template>
<script setup lang="ts">
import { router } from "@/cool";
import { ref } from "vue";
import Comment from "./comment.uvue";
import Info from "./info.uvue";
import Desc from "./desc.uvue";
import Topbar from "./topbar.uvue";
const bannerList = ref<string[]>([
"https://unix.cool-js.com/images/demo/goods/banner-1.jpg",
"https://unix.cool-js.com/images/demo/goods/banner-2.jpg",
"https://unix.cool-js.com/images/demo/goods/banner-3.jpg",
"https://unix.cool-js.com/images/demo/goods/banner-4.jpg"
]);
</script>
<style lang="scss" scoped>
.card {
@apply p-4 bg-white mb-3;
.row {
@apply flex flex-row items-center py-4;
&.is-border {
@apply border-b border-t-0 border-l-0 border-r-0 border-solid border-surface-100;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<view class="flex">
<cl-text :pt="{ className: 'text-lg font-bold mb-3' }"
>Apple/苹果 iPhone 15 (A3092) 128GB 黑色 支持移动联通电信5G 双卡双待手机</cl-text
>
<view class="flex flex-row items-center mb-3">
<view class="flex flex-row items-end">
<cl-text color="error" :pt="{ className: 'font-bold text-sm' }">¥</cl-text>
<cl-text
type="amount"
:value="8499"
color="error"
currency=""
:pt="{
className: 'font-bold ml-1 text-2xl'
}"
>
</cl-text>
</view>
<cl-text color="info" :pt="{ className: 'text-sm ml-auto' }" rounded
>已售 100 件</cl-text
>
</view>
<view class="flex flex-row mb-3">
<cl-tag type="error" plain>满199减50</cl-tag>
<cl-tag type="error" plain>满299减100</cl-tag>
</view>
</view>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,124 @@
<template>
<cl-sticky>
<cl-topbar fixed safe-area-top>
<cl-tabs
:list="list"
v-model="active"
height="36px"
:gutter="20"
@change="onTabChange"
></cl-tabs>
<template #append>
<view class="h-[44px] w-[30px] flex items-center justify-center mr-1">
<cl-icon name="search-line"></cl-icon>
</view>
</template>
</cl-topbar>
</cl-sticky>
</template>
<script setup lang="ts">
import { debounce, getSafeAreaHeight, isNull } from "@/cool";
import { usePage, type ClTabsItem } from "@/uni_modules/cool-ui";
import { getCurrentInstance, onMounted, onUnmounted, ref } from "vue";
const { proxy } = getCurrentInstance()!;
const page = usePage();
// 当前激活tab
const active = ref("info");
// 滚动时激活tab
const scrollActive = ref("");
// 卡片距离顶部偏移量
const tops = ref<number[]>([]);
// tab项列表
const list = ref<ClTabsItem[]>([
{ label: "商品", value: "info" },
{ label: "评价", value: "comment" },
{ label: "详情", value: "desc" }
]);
/**
* 获取所有.card顶部坐标
*/
async function getTops(): Promise<void> {
return new Promise((resolve) => {
uni.createSelectorQuery()
.in(proxy?.$root)
.selectAll(".card")
.boundingClientRect((res) => {
const top = page.getScrollTop() - 44 - getSafeAreaHeight("top"); // 去头部高度
// 只计算有id的card
tops.value = (res as NodeInfo[])
.filter((e) => e.id != "" && !isNull(e.id))
.map((e) => (e.top ?? 0) + top);
resolve();
})
.exec();
});
}
/**
* tab切换
*/
async function onTabChange(value: string) {
// 设置滚动时激活tab
scrollActive.value = value;
// 重新获取卡片位置
await getTops();
// 查找符合当前位置的tab索引
const index = list.value.findIndex((e) => e.value == value);
if (index < 0) return;
// 滚动到对应卡片位置
page.scrollTo(tops.value[index] + 1);
}
/**
* 同步当前tab
*/
const setActive = debounce(() => {
active.value = scrollActive.value;
}, 100);
/**
* 滚动时激活tab
*/
function onScroll(top: number) {
let index = -1;
// 查找符合当前位置的tab索引
for (let i = 0; i < tops.value.length; i++) {
if (top >= tops.value[i]) {
index = i;
}
}
// 设置激活tab
if (index >= 0 && index < list.value.length) {
scrollActive.value = list.value[index].value as string;
setActive();
}
}
onMounted(() => {
// 获取卡片位置
getTops();
// 监听页面滚动
page.onScroll(onScroll);
});
onUnmounted(() => {
// 移除监听
page.offScroll(onScroll);
});
</script>

View File

@@ -0,0 +1,416 @@
<template>
<cl-page>
<cl-sticky>
<cl-topbar fixed safe-area-top :title="`${$t('购物车 ({num})', { num: list.length })}`">
<template #prepend>
<!-- #ifdef MP -->
<cl-text
:pt="{
className: 'ml-1'
}"
@tap="isDel = !isDel"
>
{{ isDel ? t("完成") : t("管理") }}
</cl-text>
<!-- #endif -->
</template>
<template #append>
<!-- #ifndef MP -->
<cl-text
:pt="{
className: 'mr-3'
}"
@tap="isDel = !isDel"
>
{{ isDel ? t("完成") : t("管理") }}
</cl-text>
<!-- #endif -->
</template>
</cl-topbar>
</cl-sticky>
<view class="p-3">
<view class="p-3 rounded-xl bg-white dark:!bg-surface-800 mb-3">
<cl-text
:pt="{
className: 'text-sm'
}"
>🔥 最新降价商品,限时优惠,抓紧抢购!</cl-text
>
</view>
<cl-list-item
v-for="(item, index) in list"
:key="item.id"
:pt="{
className: parseClass([
'rounded-2xl ',
[index == list.length - 1, 'mb-0', 'mb-3']
]),
inner: {
className: '!p-4'
}
}"
swipeable
>
<view class="flex flex-row flex-1">
<view class="flex flex-col mr-4 pt-[55rpx]" @tap="selectItem(item)">
<cl-icon
name="checkbox-circle-line"
color="primary"
:size="40"
v-if="item.checked"
></cl-icon>
<cl-icon name="checkbox-blank-circle-line" :size="40" v-else></cl-icon>
</view>
<cl-image :width="150" :height="150" :src="item.cover"></cl-image>
<view class="flex flex-col ml-3 flex-1">
<cl-text
:pt="{
className: 'mb-2 font-bold'
}"
>{{ item.name }}</cl-text
>
<view class="flex flex-row mb-2">
<view
class="bg-surface-100 dark:!bg-surface-700 rounded-md py-1 px-2 flex flex-row items-center"
>
<cl-text
:pt="{
className: 'text-xs'
}"
>{{ item.skuName }}</cl-text
>
<cl-icon name="arrow-down-s-line"></cl-icon>
</view>
</view>
<view class="flex flex-row items-center mb-2">
<cl-text
:pt="{
className: 'text-[22rpx] text-red-500 mr-[1rpx]'
}"
>¥</cl-text
>
<cl-text
:pt="{
className: 'text-red-500 text-[32rpx] mr-auto'
}"
>{{ item.price }}</cl-text
>
<view
v-if="isDel"
class="p-[8rpx] bg-red-500 rounded-lg"
@tap="delItem(index)"
>
<cl-icon name="delete-bin-line" color="white" :size="24"></cl-icon>
</view>
<view class="flex" v-else>
<cl-input-number
v-model="item.count"
:size="40"
:min="1"
:pt="{
op: {
plus: {
className: '!rounded-full'
},
minus: {
className: '!rounded-full'
},
icon: {
size: 28
}
},
value: {
className: '!w-[60rpx] rounded-full !px-0',
input: {
className: 'text-[24rpx]'
}
}
}"
></cl-input-number>
</view>
</view>
<cl-text :size="22" color="error">比加入时降¥100</cl-text>
<cl-text :size="22">满1件可换购0.5元商品</cl-text>
</view>
</view>
<template #swipe-right>
<cl-button
type="error"
:pt="{
className: '!rounded-none h-full w-[160rpx]'
}"
@tap="delItem(index)"
>{{ t("删除") }}</cl-button
>
</template>
</cl-list-item>
<cl-empty v-if="list.length == 0"></cl-empty>
</view>
<cl-footer>
<view class="flex flex-row items-center h-[70rpx]">
<cl-checkbox
active-icon="checkbox-circle-line"
inactive-icon="checkbox-blank-circle-line"
:pt="{
className: 'mr-auto'
}"
:size="28"
v-model="selectAll"
@change="onSelectAllChange"
>{{ t("全选") }}</cl-checkbox
>
<template v-if="isDel">
<cl-button
type="error"
:pt="{
className: '!px-5'
}"
@tap="delAll"
>
{{ t("删除") }}
</cl-button>
</template>
<template v-else>
<view class="flex flex-col mr-3 items-end pt-1">
<cl-text
color="info"
:pt="{
className: 'text-xs'
}"
>{{ t("合计") }}</cl-text
>
<cl-text color="error" :value="totalPrice" type="amount"></cl-text>
</view>
<cl-button
type="error"
:pt="{
className: '!px-8'
}"
@tap="toSettle"
>
{{ t("去结算") }}
</cl-button>
</template>
</view>
</cl-footer>
</cl-page>
</template>
<script lang="ts" setup>
import { parseClass, isEmpty } from "@/cool";
import { $t, t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import { computed, ref } from "vue";
const ui = useUi();
// 商品类型
type Goods = {
id: number;
name: string;
count: number;
price: number;
cover: string;
skuName: string;
checked: boolean;
};
// 商品列表
const list = ref<Goods[]>([
{
id: 1,
name: "Apple iPad",
count: 1,
price: 450,
cover: "https://img.yzcdn.cn/vant/ipad.png",
skuName: "深空灰色 128GB WLAN版",
checked: true
},
{
id: 2,
name: "Samsung Galaxy S24",
count: 2,
price: 699,
cover: "https://img.yzcdn.cn/vant/samsung.png",
skuName: "曜石黑 12GB+256GB 官方标配",
checked: false
},
{
id: 3,
name: "Sony WH-1000XM5",
count: 1,
price: 299,
cover: "https://img.yzcdn.cn/vant/sony.png",
skuName: "黑色 无线蓝牙 官方标配",
checked: false
},
{
id: 4,
name: "小米手环8",
count: 3,
price: 49,
cover: "https://img.yzcdn.cn/vant/xiaomi.png",
skuName: "曜石黑 标准版 硅胶表带",
checked: false
},
{
id: 5,
name: "Kindle Paperwhite",
count: 1,
price: 120,
cover: "https://img.yzcdn.cn/vant/kindle.png",
skuName: "黑色 8GB 官方标配",
checked: false
}
]);
// 是否全选
const selectAll = ref(false);
/**
* 选择/取消选择单个商品
* @param item 需要操作的商品
*/
function selectItem(item: Goods) {
// 切换选中状态
item.checked = !item.checked;
// 判断是否所有商品都被选中,更新全选状态
selectAll.value = list.value.every((item) => item.checked);
}
/**
* 全选/取消全选
* @param val 是否全选
*/
function onSelectAllChange(val: boolean) {
list.value.forEach((item, index, arr) => {
// item.checked = val; // 这样写,在 android 上无效
arr[index].checked = val;
});
}
// 是否处于删除模式
const isDel = ref(false);
/**
* 删除单个商品
* @param index 商品索引
*/
function delItem(index: number) {
ui.showConfirm({
title: t("温馨提示"),
message: t("确定删除该商品吗?"),
callback(action) {
if (action === "confirm") {
// 删除指定索引的商品
list.value.splice(index, 1);
ui.showToast({
message: t("删除成功")
});
}
}
});
}
/**
* 删除所有已选中的商品
*/
function delAll() {
const checked = list.value.filter((item) => item.checked);
// 如果没有选中商品,提示用户
if (isEmpty(checked)) {
return ui.showToast({
message: t("请先选择商品")
});
}
ui.showConfirm({
title: t("温馨提示"),
message: t("确定删除选中的商品吗?"),
callback(action) {
if (action == "confirm") {
// 只保留未选中的商品
list.value = list.value.filter((item) => !item.checked);
// 如果之前是全选,删除后取消全选状态
if (selectAll.value) {
selectAll.value = false;
}
ui.showToast({
message: t("删除成功")
});
}
}
});
}
/**
* 清空购物车
*/
function clear() {
list.value = [];
selectAll.value = false;
isDel.value = false;
}
/**
* 计算已选中商品的总价
*/
const totalPrice = computed(() => {
return list.value
.filter((item) => item.checked)
.reduce((acc, item) => acc + item.price * item.count, 0);
});
/**
* 结算操作
*/
function toSettle() {
// 如果没有选中商品,提示用户
if (totalPrice.value <= 0) {
return ui.showToast({
message: t("请先选择商品")
});
}
ui.showConfirm({
title: t("温馨提示"),
message: $t("您需支付 {price} 元,请确认支付", { price: totalPrice.value }),
beforeClose(action, { showLoading, close }) {
if (action == "confirm") {
showLoading();
setTimeout(() => {
ui.showToast({
message: t("支付成功")
});
close();
}, 1000);
} else {
close();
}
}
});
}
</script>

View File

@@ -0,0 +1,10 @@
export type UserAddress = {
id?: number;
contact: string;
phone: string;
province: string;
city: string;
district: string;
address: string;
isDefault: boolean;
};

View File

@@ -0,0 +1,134 @@
<template>
<view class="flex flex-col mb-5">
<cl-text :pt="{ className: 'text-lg font-bold' }">{{ t("手机登录") }}</cl-text>
<cl-text :pt="{ className: 'text-sm mt-2' }" color="info">{{
t("未注册的手机号登录成功后将自动注册")
}}</cl-text>
</view>
<view class="flex flex-col">
<view class="mb-3 flex flex-row">
<cl-input
v-model="form.phone"
prefix-icon="device-fill"
:placeholder="t('请输入手机号')"
:border="false"
:pt="{
className: parseClass([
'!h-[90rpx] flex-1 !rounded-xl !px-4',
[isDark, '!bg-surface-70', '!bg-white']
]),
prefixIcon: {
className: 'mr-1'
}
}"
></cl-input>
</view>
<view class="relative flex flex-row items-center mb-5">
<cl-input
v-model="form.smsCode"
:clearable="false"
type="number"
prefix-icon="shield-check-fill"
:placeholder="t('请输入验证码')"
:maxlength="4"
:border="false"
:pt="{
className: parseClass([
'!h-[90rpx] flex-1 !rounded-xl !px-4',
[isDark, '!bg-surface-70', '!bg-white']
]),
prefixIcon: {
className: 'mr-1'
}
}"
>
</cl-input>
<view class="absolute right-0">
<sms-btn
:ref="refs.set('smsBtn')"
:phone="form.phone"
@success="showCode = true"
></sms-btn>
</view>
</view>
<cl-button
:pt="{
className: '!h-[90rpx] !rounded-xl'
}"
:loading="loading"
:disabled="disabled"
@tap="toLogin"
>
{{ t("登录") }}
</cl-button>
</view>
</template>
<script setup lang="ts">
import { t } from "@/locale";
import { computed, inject, ref, type PropType } from "vue";
import type { LoginForm } from "../../types";
import SmsBtn from "@/components/sms-btn.uvue";
import { isDark, parseClass, request, useRefs, type Response } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
const props = defineProps({
form: {
type: Object as PropType<LoginForm>,
default: () => ({})
}
});
const emit = defineEmits(["success"]);
const ui = useUi();
const refs = useRefs();
// 是否同意
const isAgree = inject("isAgree") as () => boolean;
// 是否显示验证码
const showCode = ref(false);
// 是否加载中
const loading = ref(false);
// 是否禁用
const disabled = computed(() => {
return props.form.phone == "" || props.form.smsCode == "";
});
// 登录
async function toLogin() {
if (!isAgree()) {
return;
}
const { phone, smsCode } = props.form;
loading.value = true;
await request({
url: "/app/user/login/phone",
method: "POST",
data: {
phone,
smsCode
}
})
.then((res) => {
emit("success", res);
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
});
loading.value = false;
}
</script>

View File

@@ -0,0 +1,242 @@
<template>
<cl-popup
v-model="editVisible"
direction="center"
:title="t('提示')"
size="80%"
@close="onEditClose"
>
<view class="p-4 pt-0">
<cl-text color="info" :pt="{ className: 'text-sm' }">
{{ t("为提供更好的服务,我们邀请您填写昵称、头像等公开信息") }}
</cl-text>
<view
class="flex flex-row justify-between items-center bg-surface-100 rounded-xl p-2 px-3 mt-3 h-[95rpx]"
>
<cl-text>{{ t("头像") }}</cl-text>
<view class="relative">
<cl-avatar :size="60" :src="editForm.avatarUrl"></cl-avatar>
<button
class="absolute top-0 right-0 h-10 w-10 z-10 opacity-0 p-0 m-0"
open-type="chooseAvatar"
@chooseavatar="onEditChooseAvatar"
></button>
</view>
</view>
<view
class="flex flex-row justify-between items-center bg-surface-100 rounded-xl p-2 px-3 mt-3 h-[95rpx]"
>
<cl-text>{{ t("昵称") }}</cl-text>
<cl-input
v-model="editForm.nickName"
type="nickname"
:border="false"
:placeholder="t('点击输入昵称')"
:maxlength="16"
:pt="{
className: '!bg-transparent !px-0 flex-1',
inner: {
className: 'text-right'
}
}"
></cl-input>
</view>
<view class="flex flex-row mt-4">
<cl-button
size="large"
text
border
type="light"
:pt="{
className: 'flex-1 !rounded-xl h-[80rpx]'
}"
@tap="editClose"
>{{ t("取消") }}</cl-button
>
<cl-button
size="large"
:pt="{
className: 'flex-1 !rounded-xl h-[80rpx]'
}"
:loading="editLoading"
@tap="editSave"
>{{ t("确认") }}</cl-button
>
</view>
</view>
</cl-popup>
</template>
<script setup lang="ts">
import {
parse,
request,
router,
upload,
userInfo,
useStore,
useWx,
type Response,
type Token
} from "@/cool";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import { reactive, ref } from "vue";
const emit = defineEmits(["success"]);
const { user } = useStore();
const ui = useUi();
const wx = useWx();
// 是否显示编辑
const editVisible = ref(false);
// 是否保存中
const editLoading = ref(false);
// 编辑表单
type EditForm = {
avatarUrl: string;
nickName: string;
};
const editForm = reactive<EditForm>({
avatarUrl: "",
nickName: ""
});
// 编辑打开
function editOpen() {
editVisible.value = true;
}
// 编辑关闭
function editClose() {
editVisible.value = false;
}
// 编辑保存
async function editSave() {
// 校验头像是否已上传
if (editForm.avatarUrl == "") {
ui.showToast({
message: t("请上传头像")
});
return;
}
// 校验昵称是否已填写
if (editForm.nickName == "") {
ui.showToast({
message: t("请输入昵称")
});
return;
}
// 设置保存状态为加载中
editLoading.value = true;
// 上传头像并更新用户信息
await upload(editForm.avatarUrl)
.then((url) => {
// 上传成功后,更新用户昵称和头像
user.update({
nickName: editForm.nickName,
avatarUrl: url
});
// 关闭弹窗
editClose();
// 跳转首页
router.nextLogin();
})
.catch((err) => {
// 上传失败,提示错误信息
ui.showToast({
message: (err as Response).message!
});
});
// 恢复保存状态
editLoading.value = false;
}
// 编辑选择头像
function onEditChooseAvatar(e: UniEvent) {
// #ifdef MP-WEIXIN
editForm.avatarUrl = e.detail.avatarUrl;
// #endif
}
// 编辑关闭
function onEditClose() {
editVisible.value = false;
}
// 微信小程序登录
async function miniLogin() {
// #ifdef MP
ui.showLoading(t("登录中"));
await wx.miniLogin().then(async (data) => {
await request({
url: "/app/user/login/mini",
method: "POST",
data
})
.then(async (res) => {
// 设置token
user.setToken(parse<Token>(res)!);
// 获取用户信息
await user.get();
// 是否首次注册,根据业务情况调整判断逻辑
if (userInfo.value?.nickName == "微信用户") {
// 打开编辑弹窗
editOpen();
} else {
// 跳转首页
router.nextLogin();
}
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
});
});
ui.hideLoading();
// #endif
}
// 微信APP登录
function appLogin() {
// 开发中
}
// 微信登录
async function login() {
// #ifdef MP
miniLogin();
// #endif
// #ifdef APP
appLogin();
// #endif
}
defineExpose({
login,
editOpen,
editClose
});
</script>

View File

@@ -0,0 +1,52 @@
<template>
<cl-page>
<cl-topbar safe-area-top :title="t('编辑简介')" background-color="transparent"> </cl-topbar>
<view class="p-3">
<cl-textarea
v-model="content"
:placeholder="t('介绍一下自己')"
:border="false"
:height="200"
>
</cl-textarea>
</view>
<cl-footer>
<cl-button size="large" :disabled="content == ''" @tap="confirm">{{
t("确认")
}}</cl-button>
</cl-footer>
</cl-page>
</template>
<script setup lang="ts">
import { router, userInfo, useStore } from "@/cool";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const { user } = useStore();
// 输入框内容
const content = ref("");
async function confirm() {
if (content.value == "") {
return ui.showToast({
message: t("简介不能为空")
});
}
await user.update({
description: content.value
});
router.back();
}
onReady(() => {
content.value = userInfo.value?.description ?? "";
});
</script>

View File

@@ -0,0 +1,80 @@
<template>
<cl-page>
<cl-topbar safe-area-top :title="t('编辑昵称')" background-color="transparent"> </cl-topbar>
<view class="p-3">
<cl-input
v-model="content"
autofocus
:placeholder="t('请输入昵称')"
:border="false"
:pt="{
className: '!h-[80rpx]'
}"
>
<template #append>
<cl-text color="info" :pt="{ className: 'text-sm ml-2' }"
>{{ content.length }}/20</cl-text
>
</template>
</cl-input>
<view class="p-3">
<cl-text color="info" :pt="{ className: 'text-sm' }">{{
t("请设置2-20个字符不包括@<>/等无效字符")
}}</cl-text>
</view>
</view>
<cl-footer>
<cl-button size="large" :disabled="content == ''" @tap="confirm">{{
t("确认")
}}</cl-button>
</cl-footer>
</cl-page>
</template>
<script setup lang="ts">
import { router, userInfo, useStore } from "@/cool";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const { user } = useStore();
// 输入框内容
const content = ref("");
// 确认按钮点击事件
async function confirm() {
// 检查昵称长度和特殊字符
if (content.value.length < 2 || content.value.length > 20) {
ui.showToast({
message: t("昵称长度需在2-20个字符之间")
});
return;
}
// 正则匹配 - 不允许特殊字符@<>/等
const reg = /^[^@<>\/]*$/;
if (!reg.test(content.value)) {
ui.showToast({
message: t("昵称不能包含@<>/等特殊字符")
});
return;
}
// 更新用户昵称
await user.update({
nickName: content.value
});
router.back();
}
// 页面加载时,设置输入框内容
onReady(() => {
content.value = userInfo.value?.nickName ?? "";
});
</script>

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