热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专为iOS开发场景设计,能够根据用户提供的界面元素描述,生成专业、准确、结构化的技术文档。通过系统化的分析流程,确保输出的文档包含元素功能定义、技术实现方案、交互行为描述和最佳实践建议,帮助开发者快速理解和实现iOS界面组件。文档采用标准化的技术写作风格,避免主观表述,专注于提供可操作的技术指导。
以下为完整的参考实现,兼容 iOS 13+。在 iOS 15+ 上优先使用 UIButton.Configuration 实现活动指示器与状态样式;在 iOS 13–14 使用自定义子视图实现菊花。
import UIKit
protocol AnalyticsLogging {
func log(event: String, properties: [String: Any]?)
}
final class LoginButton: UIButton {
// MARK: - Public API
// 点击回调(由控制器注入具体登录逻辑)
var onTap: (() -> Void)?
// 2s 超时任务
private var timeoutWorkItem: DispatchWorkItem?
// 当前是否加载中
private(set) var isLoading = false
// 提供外部更新启用态的方法(结合表单校验结果)
func updateEnabledState(canSubmit: Bool) {
guard !isLoading else {
// 加载中强制禁用
isEnabled = false
return
}
isEnabled = canSubmit
}
// 开始加载(默认2秒超时)
func startLoading(timeout: TimeInterval = 2.0) {
guard !isLoading else { return }
isLoading = true
isUserInteractionEnabled = false
isEnabled = false
applyLoadingAppearance(true)
// VoiceOver 提示
UIAccessibility.post(notification: .announcement, argument: NSLocalizedString("正在登录", comment: "正在登录"))
// 超时恢复
timeoutWorkItem?.cancel()
let work = DispatchWorkItem { [weak self] in
self?.stopLoading(restoreEnabledFromValidation: true)
}
timeoutWorkItem = work
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: work)
}
// 停止加载
// restoreEnabledFromValidation: true 时,按钮启用态应由外部再次根据表单校验结果更新
func stopLoading(restoreEnabledFromValidation: Bool = true) {
timeoutWorkItem?.cancel()
timeoutWorkItem = nil
isLoading = false
isUserInteractionEnabled = true
applyLoadingAppearance(false)
// 移除加载状态的辅助功能值
accessibilityValue = nil
// 启用态恢复交给外部(表单校验)更安全
if !restoreEnabledFromValidation {
isEnabled = true
}
}
// MARK: - Init
private let spinner: UIActivityIndicatorView = {
let v = UIActivityIndicatorView(style: .medium)
v.hidesWhenStopped = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - Setup
private func setup() {
isAccessibilityElement = true
accessibilityTraits = [.button]
accessibilityLabel = NSLocalizedString("auth_login", comment: "Login")
layer.cornerRadius = 12
layer.masksToBounds = true
// 字体:系统 17,随 Dynamic Type 缩放
let baseFont = UIFont.systemFont(ofSize: 17, weight: .regular)
titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: baseFont)
titleLabel?.adjustsFontForContentSizeCategory = true
titleLabel?.lineBreakMode = .byTruncatingTail
setTitle(NSLocalizedString("auth_login", comment: "Login"), for: .normal)
if #available(iOS 15.0, *) {
var cfg = UIButton.Configuration.filled()
cfg.title = NSLocalizedString("auth_login", comment: "Login")
cfg.baseForegroundColor = .white
cfg.baseBackgroundColor = UIColor(named: "AppPrimary")
cfg.cornerStyle = .fixed
cfg.background.cornerRadius = 12
configuration = cfg
configurationUpdateHandler = { [weak self] btn in
guard let self = self else { return }
var c = btn.configuration!
c.baseBackgroundColor = btn.isEnabled ? UIColor(named: "AppPrimary") : UIColor(named: "AppPrimaryDisabled")
btn.configuration = c
}
} else {
// iOS 13–14 样式
backgroundColor = UIColor(named: "AppPrimary")
setTitleColor(.white, for: .normal)
addSubview(spinner)
NSLayoutConstraint.activate([
spinner.centerXAnchor.constraint(equalTo: centerXAnchor),
spinner.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
// 交互
addTarget(self, action: #selector(didTouchDown), for: .touchDown)
addTarget(self, action: #selector(didTap), for: .touchUpInside)
addTarget(self, action: #selector(cancelPress), for: [.touchDragExit, .touchCancel, .touchUpOutside])
// 不添加任何长按手势;默认无长按动作
}
// MARK: - Loading appearance
private func applyLoadingAppearance(_ loading: Bool) {
if #available(iOS 15.0, *) {
if var cfg = configuration {
cfg.showsActivityIndicator = loading
configuration = cfg
}
} else {
if loading {
titleLabel?.alpha = 0.0
spinner.color = titleColor(for: .normal)
spinner.startAnimating()
} else {
spinner.stopAnimating()
titleLabel?.alpha = 1.0
}
}
// 无障碍:loading 状态提示
accessibilityValue = loading ? NSLocalizedString("加载中", comment: "加载中") : nil
}
// MARK: - Touch animations & haptics
@objc private func didTouchDown() {
// 轻触回弹:按下缩小,松开弹回
UIView.animate(withDuration: 0.08) {
self.transform = CGAffineTransform(scaleX: 0.96, y: 0.96)
}
}
@objc private func cancelPress() {
UIView.animate(withDuration: 0.2,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 2,
options: [.allowUserInteraction, .beginFromCurrentState],
animations: { self.transform = .identity },
completion: nil)
}
@objc private func didTap() {
// 轻震
let generator = UIImpactFeedbackGenerator(style: .light)
generator.prepare()
generator.impactOccurred()
cancelPress()
onTap?()
}
}
final class LoginViewController: UIViewController {
private let phoneField = UITextField()
private let codeField = UITextField()
private let loginButton = LoginButton()
// 埋点
private let analytics: AnalyticsLogging
private let userID: String
init(analytics: AnalyticsLogging, userID: String) {
self.analytics = analytics
self.userID = userID
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
// 配置文本框(略),添加 editingChanged 事件
phoneField.keyboardType = .numberPad
codeField.keyboardType = .numberPad
phoneField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
codeField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
// 布局
layoutUI()
// 点击处理
loginButton.onTap = { [weak self] in
guard let self = self else { return }
self.analytics.log(event: "auth_login_click", properties: ["user_id": self.userID])
self.loginButton.startLoading()
self.submitLogin { result in
// 主线程更新 UI
DispatchQueue.main.async {
switch result {
case .success:
self.analytics.log(event: "auth_login_success", properties: ["user_id": self.userID])
self.loginButton.stopLoading(restoreEnabledFromValidation: true)
case .failure:
self.analytics.log(event: "auth_login_failure", properties: ["user_id": self.userID])
self.loginButton.stopLoading(restoreEnabledFromValidation: true)
}
// 停止加载后,依据最新表单校验结果恢复启用态
self.updateLoginButtonEnabled()
}
}
}
// 初始校验
updateLoginButtonEnabled()
}
@objc private func textDidChange() {
updateLoginButtonEnabled()
}
private func updateLoginButtonEnabled() {
let phone = phoneField.text ?? ""
let code = codeField.text ?? ""
let canSubmit = isValidPhone(phone) && isValidCode(code)
loginButton.updateEnabledState(canSubmit: canSubmit)
}
// 根据业务规则实现(示例仅占位,替换为实际校验)
private func isValidPhone(_ text: String) -> Bool {
// TODO: 替换为真实的手机号校验规则
return text.count >= 6
}
private func isValidCode(_ text: String) -> Bool {
// TODO: 替换为真实的验证码校验规则
return text.count >= 4
}
// 模拟登录请求(替换为真实网络调用)
private func submitLogin(completion: @escaping (Result<Void, Error>) -> Void) {
// 示例:1s 后回调成功/失败
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
completion(.success(()))
}
}
private func layoutUI() {
view.addSubview(phoneField)
view.addSubview(codeField)
view.addSubview(loginButton)
phoneField.translatesAutoresizingMaskIntoConstraints = false
codeField.translatesAutoresizingMaskIntoConstraints = false
loginButton.translatesAutoresizingMaskIntoConstraints = false
let guide = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 仅示例:phoneField 与 codeField 的布局请按实际页面完成
phoneField.topAnchor.constraint(equalTo: guide.topAnchor, constant: 40),
phoneField.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 16),
phoneField.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -16),
codeField.topAnchor.constraint(equalTo: phoneField.bottomAnchor, constant: 12),
codeField.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 16),
codeField.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -16),
// 登录按钮布局:高度48,左右安全边距16,与验证码输入框间距12
loginButton.topAnchor.constraint(equalTo: codeField.bottomAnchor, constant: 12),
loginButton.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 16),
loginButton.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -16),
loginButton.heightAnchor.constraint(equalToConstant: 48)
])
}
}
A reusable UICollectionViewCell that displays a product card in a two-column grid. The cell contains:
import UIKit
// MARK: - View Models / Config
struct ProductCardViewModel: Hashable {
let id: String
let title: String
let imageURL: URL?
let rating: Double? // e.g., 4.6
let monthlySales: Int? // e.g., 1200
let currencyCode: String // e.g., "USD"
let locale: Locale // e.g., Locale.current
let currentPrice: Decimal
let originalPrice: Decimal?
let promoText: String? // e.g., "SALE", nil to hide
}
struct ProductCardUIFlags {
let hideOriginalPrice: Bool
}
final class ImageLoader {
static let shared = ImageLoader()
private let cache = NSCache<NSURL, UIImage>()
private var tasks = [UIImageView: URLSessionDataTask]()
func load(_ url: URL?, into imageView: UIImageView, placeholder: UIImage? = nil, crossfade: Bool = true) {
// Cancel in-flight
tasks[imageView]?.cancel()
imageView.image = placeholder
guard let url else { return }
if let cached = cache.object(forKey: url as NSURL) {
imageView.image = cached
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self, weak imageView] data, _, _ in
guard let self, let imageView, let data, let image = UIImage(data: data) else { return }
self.cache.setObject(image, forKey: url as NSURL)
DispatchQueue.main.async {
if crossfade {
UIView.transition(with: imageView, duration: 0.25, options: .transitionCrossDissolve) {
imageView.image = image
}
} else {
imageView.image = image
}
}
}
tasks[imageView] = task
task.resume()
}
func cancel(for imageView: UIImageView) {
tasks[imageView]?.cancel()
tasks[imageView] = nil
}
}
// Optional: if you need true progressive decoding, consider a proven library (e.g., SDWebImage) with progressive loading.
// MARK: - Skeleton Shimmer
final class SkeletonView: UIView {
private let gradient = CAGradientLayer()
private var animating = false
override class var layerClass: AnyClass { CAGradientLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = false
setup()
}
required init?(coder: NSCoder) { fatalError() }
private func setup() {
let gradient = self.layer as! CAGradientLayer
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
gradient.colors = [
UIColor.systemFill.withAlphaComponent(0.6).cgColor,
UIColor.systemFill.withAlphaComponent(0.3).cgColor,
UIColor.systemFill.withAlphaComponent(0.6).cgColor
]
gradient.locations = [0, 0.5, 1]
layer.cornerRadius = 8
clipsToBounds = true
isHidden = true
}
func start() {
guard !animating else { return }
isHidden = false
animating = true
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [ -1, -0.5, 0 ]
animation.toValue = [ 1, 1.5, 2 ]
animation.duration = 1.2
animation.repeatCount = .infinity
(layer as! CAGradientLayer).add(animation, forKey: "skeleton")
}
func stop() {
animating = false
(layer as! CAGradientLayer).removeAnimation(forKey: "skeleton")
isHidden = true
}
}
// MARK: - ProductCardCell
protocol ProductCardCellDelegate: AnyObject {
func productCardCellDidTapAddToCart(_ cell: ProductCardCell, productID: String)
}
final class ProductCardCell: UICollectionViewCell {
// Public
weak var delegate: ProductCardCellDelegate?
var onAddToCart: ((String) -> Void)? // alternative callback
private(set) var productID: String?
// UI
private let container = UIView()
private let imageView = UIImageView()
private let promoBadge = UILabel()
private let titleLabel = UILabel()
private let ratingStack = UIStackView()
private let starImageView = UIImageView()
private let ratingLabel = UILabel()
private let priceStack = UIStackView()
private let currentPriceLabel = UILabel()
private let originalPriceLabel = UILabel()
private let addButton = UIButton(type: .system)
private let imageSkeleton = SkeletonView()
private let textSkeleton1 = SkeletonView()
private let textSkeleton2 = SkeletonView()
// Constraints helpers
private var imageAspect: NSLayoutConstraint!
// Formatting
private lazy var priceFormatter: NumberFormatter = {
let nf = NumberFormatter()
nf.numberStyle = .currency
return nf
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupLayout()
setupAccessibility()
contentView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
}
required init?(coder: NSCoder) { fatalError() }
override func prepareForReuse() {
super.prepareForReuse()
ImageLoader.shared.cancel(for: imageView)
imageView.image = nil
promoBadge.isHidden = true
originalPriceLabel.isHidden = true
titleLabel.text = nil
ratingLabel.text = nil
currentPriceLabel.text = nil
stopSkeleton()
startSkeleton()
}
// MARK: Setup
private func setupUI() {
contentView.backgroundColor = .secondarySystemBackground
contentView.layer.cornerRadius = 12
contentView.layer.masksToBounds = true
container.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(container)
// Image
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
// Promo badge
promoBadge.font = .preferredFont(forTextStyle: .caption2)
promoBadge.adjustsFontForContentSizeCategory = true
promoBadge.textColor = .white
promoBadge.backgroundColor = .systemRed
promoBadge.layer.cornerRadius = 4
promoBadge.clipsToBounds = true
promoBadge.textAlignment = .center
promoBadge.setContentHuggingPriority(.required, for: .horizontal)
promoBadge.setContentCompressionResistancePriority(.required, for: .horizontal)
promoBadge.translatesAutoresizingMaskIntoConstraints = false
// Title
titleLabel.font = .preferredFont(forTextStyle: .body)
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.numberOfLines = 2
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.translatesAutoresizingMaskIntoConstraints = false
// Rating
ratingStack.axis = .horizontal
ratingStack.spacing = 4
ratingStack.alignment = .center
ratingStack.translatesAutoresizingMaskIntoConstraints = false
ratingStack.isLayoutMarginsRelativeArrangement = false
starImageView.image = UIImage(systemName: "star.fill")
starImageView.tintColor = .systemYellow
starImageView.setContentHuggingPriority(.required, for: .horizontal)
starImageView.translatesAutoresizingMaskIntoConstraints = false
starImageView.widthAnchor.constraint(equalToConstant: 14).isActive = true
starImageView.heightAnchor.constraint(equalToConstant: 14).isActive = true
ratingLabel.font = .preferredFont(forTextStyle: .footnote)
ratingLabel.adjustsFontForContentSizeCategory = true
ratingLabel.textColor = .secondaryLabel
ratingLabel.translatesAutoresizingMaskIntoConstraints = false
ratingStack.addArrangedSubview(starImageView)
ratingStack.addArrangedSubview(ratingLabel)
// Price row
priceStack.axis = .horizontal
priceStack.alignment = .firstBaseline
priceStack.spacing = 6
priceStack.translatesAutoresizingMaskIntoConstraints = false
currentPriceLabel.font = .preferredFont(forTextStyle: .headline)
currentPriceLabel.adjustsFontForContentSizeCategory = true
currentPriceLabel.textColor = .label
originalPriceLabel.font = .preferredFont(forTextStyle: .subheadline)
originalPriceLabel.adjustsFontForContentSizeCategory = true
originalPriceLabel.textColor = .secondaryLabel
priceStack.addArrangedSubview(currentPriceLabel)
priceStack.addArrangedSubview(originalPriceLabel)
// Add button
var config = UIButton.Configuration.filled()
config.title = NSLocalizedString("Add to Cart", comment: "Add to Cart button")
config.image = UIImage(systemName: "cart.badge.plus")
config.imagePlacement = .leading
config.imagePadding = 6
config.cornerStyle = .medium
addButton.configuration = config
addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
addButton.translatesAutoresizingMaskIntoConstraints = false
// Skeletons
[imageSkeleton, textSkeleton1, textSkeleton2].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.isHidden = true
}
// Assemble
container.addSubview(imageView)
container.addSubview(promoBadge)
container.addSubview(titleLabel)
container.addSubview(ratingStack)
container.addSubview(priceStack)
container.addSubview(addButton)
container.addSubview(imageSkeleton)
container.addSubview(textSkeleton1)
container.addSubview(textSkeleton2)
// RTL considerations
ratingStack.semanticContentAttribute = .forceLeftToRight // stars should not reverse
}
private func setupLayout() {
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: contentView.topAnchor),
container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
container.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
imageView.topAnchor.constraint(equalTo: container.topAnchor),
imageView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
])
imageAspect = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 9.0/16.0)
imageAspect.isActive = true
// Promo badge top-trailing over image
NSLayoutConstraint.activate([
promoBadge.topAnchor.constraint(equalTo: imageView.topAnchor, constant: 8),
promoBadge.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: -8)
])
// Below image
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
ratingStack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
ratingStack.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
ratingStack.trailingAnchor.constraint(lessThanOrEqualTo: titleLabel.trailingAnchor),
priceStack.topAnchor.constraint(equalTo: ratingStack.bottomAnchor, constant: 8),
priceStack.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
priceStack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8),
addButton.centerYAnchor.constraint(equalTo: priceStack.firstBaselineAnchor),
addButton.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
addButton.leadingAnchor.constraint(greaterThanOrEqualTo: priceStack.trailingAnchor, constant: 8)
])
// Skeleton placements
NSLayoutConstraint.activate([
imageSkeleton.topAnchor.constraint(equalTo: imageView.topAnchor),
imageSkeleton.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
imageSkeleton.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
imageSkeleton.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
textSkeleton1.topAnchor.constraint(equalTo: titleLabel.topAnchor),
textSkeleton1.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
textSkeleton1.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
textSkeleton1.heightAnchor.constraint(equalToConstant: 14),
textSkeleton2.topAnchor.constraint(equalTo: textSkeleton1.bottomAnchor, constant: 6),
textSkeleton2.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
textSkeleton2.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: -40),
textSkeleton2.heightAnchor.constraint(equalToConstant: 14),
])
}
// MARK: Configure
func configure(with model: ProductCardViewModel, uiFlags: ProductCardUIFlags) {
self.productID = model.id
// Image loading with skeleton
startSkeleton()
ImageLoader.shared.load(model.imageURL, into: imageView, placeholder: nil, crossfade: true)
// Stop skeleton when image settles; a simple approach is to stop in layoutSubviews after image is set.
// Here we stop after a short async tick to allow crossfade.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
guard let self else { return }
if self.imageView.image != nil { self.stopSkeleton() }
}
// Promo
if let promo = model.promoText, !promo.isEmpty {
promoBadge.isHidden = false
promoBadge.text = " \(promo) "
} else {
promoBadge.isHidden = true
}
// Title
titleLabel.text = model.title
// Rating + monthly sales
if let rating = model.rating, let sales = model.monthlySales {
ratingLabel.text = String(format: "%.1f · %@/mo", rating, Self.formatSales(sales, locale: model.locale))
ratingStack.isHidden = false
} else {
ratingLabel.text = nil
ratingStack.isHidden = true
}
// Prices
priceFormatter.locale = model.locale
priceFormatter.currencyCode = model.currencyCode
currentPriceLabel.text = priceFormatter.string(from: model.currentPrice as NSDecimalNumber)
if let original = model.originalPrice,
!uiFlags.hideOriginalPrice,
original > model.currentPrice,
let originalStr = priceFormatter.string(from: original as NSDecimalNumber) {
let attrs: [NSAttributedString.Key: Any] = [
.strikethroughStyle: NSUnderlineStyle.single.rawValue,
.foregroundColor: UIColor.secondaryLabel
]
originalPriceLabel.attributedText = NSAttributedString(string: originalStr, attributes: attrs)
originalPriceLabel.isHidden = false
} else {
originalPriceLabel.isHidden = true
}
updateAccessibility(model: model, hasOriginalPrice: !originalPriceLabel.isHidden)
}
private static func formatSales(_ sales: Int, locale: Locale) -> String {
// Simple compact formatting (e.g., 1.2K)
let fmt = NumberFormatter()
fmt.locale = locale
fmt.numberStyle = .decimal
if sales >= 1000 {
let k = Double(sales) / 1000.0
return String(format: locale.identifier.hasPrefix("zh") ? "%.1fK" : "%.1fK", k)
}
return fmt.string(from: sales as NSNumber) ?? "\(sales)"
}
// MARK: Actions
@objc private func addTapped() {
// Haptic
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
// Button micro-interaction
UIView.animate(withDuration: 0.08, animations: {
self.addButton.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
}, completion: { _ in
UIView.animate(withDuration: 0.2,
delay: 0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 0.8,
options: .allowUserInteraction,
animations: {
self.addButton.transform = .identity
}, completion: nil)
})
guard let id = productID else { return }
// Delegate/callback for inventory validation in VC
delegate?.productCardCellDidTapAddToCart(self, productID: id)
onAddToCart?(id)
}
// Called by VC after inventory check result (success path)
func playAddToCartSuccessAnimation(to cartTarget: CGPoint? = nil, in container: UIView? = nil) {
// Optional "fly-to-cart" animation
guard let snapshot = imageView.snapshotView(afterScreenUpdates: false),
let root = container ?? window else { return }
let startFrame = imageView.convert(imageView.bounds, to: root)
snapshot.frame = startFrame
snapshot.layer.cornerRadius = imageView.layer.cornerRadius
snapshot.layer.masksToBounds = true
root.addSubview(snapshot)
let endPoint = cartTarget ?? CGPoint(x: startFrame.maxX, y: startFrame.minY - 40)
UIView.animate(withDuration: 0.7, delay: 0, options: .curveEaseIn) {
snapshot.frame = CGRect(origin: endPoint, size: CGSize(width: 10, height: 10))
snapshot.alpha = 0.2
} completion: { _ in
snapshot.removeFromSuperview()
}
}
// MARK: Skeleton helpers
private func startSkeleton() {
[imageSkeleton, textSkeleton1, textSkeleton2].forEach { $0.start() }
}
private func stopSkeleton() {
[imageSkeleton, textSkeleton1, textSkeleton2].forEach { $0.stop() }
}
// MARK: Accessibility
private func setupAccessibility() {
isAccessibilityElement = true
accessibilityTraits.insert(.button) // acts like a composite with add action
accessibilityCustomActions = [
UIAccessibilityCustomAction(name: NSLocalizedString("Add to Cart", comment: "VO action"), target: self, selector: #selector(addTapped))
]
}
private func updateAccessibility(model: ProductCardViewModel, hasOriginalPrice: Bool) {
let priceText = currentPriceLabel.text ?? ""
var parts: [String] = [model.title, priceText]
if hasOriginalPrice, let original = originalPriceLabel.attributedText?.string {
parts.append(NSLocalizedString("Original", comment: "Original price prefix") + " " + original)
}
if let rating = model.rating {
let ratingStr = String(format: NSLocalizedString("Rating %.1f", comment: "Rating value"), rating)
parts.append(ratingStr)
}
accessibilityLabel = parts.joined(separator: ", ")
}
}
// MARK: - Two-column Compositional Layout (iOS 13+)
enum ProductGridLayoutBuilder {
static func twoColumnLayout(interItemSpacing: CGFloat = 8,
contentInsets: NSDirectionalEdgeInsets = .init(top: 8, leading: 8, bottom: 8, trailing: 8)
) -> UICollectionViewCompositionalLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
heightDimension: .estimated(320))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: interItemSpacing/2, leading: interItemSpacing/2, bottom: interItemSpacing/2, trailing: interItemSpacing/2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(320))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = contentInsets
return UICollectionViewCompositionalLayout(section: section)
}
}
Key configuration parameters:
Limitations:
Integration sketch (in a view controller):
// 1) Layout
collectionView.setCollectionViewLayout(ProductGridLayoutBuilder.twoColumnLayout(), animated: false)
collectionView.register(ProductCardCell.self, forCellWithReuseIdentifier: "ProductCell")
// 2) Cell configuration (diffable data source example omitted)
func configure(cell: ProductCardCell, with model: ProductCardViewModel, flags: ProductCardUIFlags) {
cell.configure(with: model, uiFlags: flags)
cell.delegate = self
cell.onAddToCart = { [weak self] productID in
self?.checkInventoryAndAdd(productID: productID) { success, cartPoint in
if success {
cell.playAddToCartSuccessAnimation(to: cartPoint, in: self?.view)
} else {
// show out-of-stock feedback
}
}
}
}
This implementation adheres to UIKit best practices, supports accessibility, RTL, Dark Mode, localized currency formatting, and provides the required interactions and layout behavior for a production-grade product card cell.
聊天输入栏作为 inputAccessoryView,随键盘出现/隐藏自动贴合底部安全区。核心能力包含:
下面给出基于 UIKit + Swift 的参考实现,便于培训与快速落地。最低系统建议 iOS 13+。
import UIKit
protocol ChatInputBarDelegate: AnyObject {
func inputBarDidTapSend(_ bar: ChatInputBar, text: String)
func inputBarDidTapAttach(_ bar: ChatInputBar)
func inputBarDidToggleVoice(_ bar: ChatInputBar, isVoiceMode: Bool)
func inputBar(_ bar: ChatInputBar, didPasteImages images: [UIImage])
func inputBar(_ bar: ChatInputBar, didChangeText text: String)
func inputBar(_ bar: ChatInputBar, didTriggerSuggestion trigger: ChatInputBar.SuggestionTrigger?)
}
final class PasteAwareTextView: UITextView {
weak var pasteImageHandler: (([UIImage]) -> Void)?
override func paste(_ sender: Any?) {
let pb = UIPasteboard.general
if let images = pb.images, !images.isEmpty {
pasteImageHandler?(images)
} else {
super.paste(sender)
}
}
}
final class ChatInputBar: UIView, UITextViewDelegate {
// 触发器类型:@ 或 /
enum SuggestionTrigger {
case mention(query: String) // @xxx
case command(query: String) // /xxx
}
weak var delegate: ChatInputBarDelegate?
// 可设置:是否回车即发送
var sendOnReturn: Bool = UserDefaults.standard.bool(forKey: "SendOnReturn") {
didSet { textView.returnKeyType = sendOnReturn ? .send : .default }
}
// UI 元素
private let attachButton = UIButton(type: .system)
private let voiceButton = UIButton(type: .system)
private let sendButton = UIButton(type: .system)
private let textView = PasteAwareTextView()
// 占位符
private let placeholderLabel = UILabel()
// 样式与布局
private let contentStack = UIStackView()
private var textViewHeightConstraint: NSLayoutConstraint!
// 行高控制
private var minTextHeight: CGFloat { ceil(textLineHeight + textInsets) }
private var maxTextHeight: CGFloat { ceil(textLineHeight * 6 + textInsets) }
private var textLineHeight: CGFloat { textView.font?.lineHeight ?? 17 }
private var textInsets: CGFloat {
textView.textContainerInset.top + textView.textContainerInset.bottom
}
// 自适应高度
override var intrinsicContentSize: CGSize {
// 让系统使用自动布局结果 + 底部安全区
let fitting = contentStack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
return CGSize(width: UIView.noIntrinsicMetric, height: fitting.height + safeAreaInsets.bottom)
}
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
invalidateIntrinsicContentSize()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupAccessibility()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
setupAccessibility()
}
private func setupUI() {
backgroundColor = .secondarySystemBackground
preservesSuperviewLayoutMargins = true
layoutMargins = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)
// 按钮基础配置(SF Symbols,自动深色适配)
attachButton.setImage(UIImage(systemName: "paperclip"), for: .normal)
voiceButton.setImage(UIImage(systemName: "mic"), for: .normal)
sendButton.setImage(UIImage(systemName: "paperplane.fill"), for: .normal)
sendButton.tintColor = .systemBlue
// 可点击区域建议 >= 44pt
[attachButton, voiceButton, sendButton].forEach {
$0.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
$0.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
$0.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
}
// 文字视图
textView.backgroundColor = .systemBackground
textView.layer.cornerRadius = 8
textView.textContainer.lineFragmentPadding = 8
textView.textContainerInset = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
textView.isScrollEnabled = false
textView.delegate = self
textView.font = .preferredFont(forTextStyle: .body)
textView.adjustsFontForContentSizeCategory = true
textView.returnKeyType = sendOnReturn ? .send : .default
textView.pasteImageHandler = { [weak self] images in
guard let self = self else { return }
self.delegate?.inputBar(self, didPasteImages: images)
}
// 占位符
placeholderLabel.text = NSLocalizedString("send_message_placeholder", comment: "发送消息……")
placeholderLabel.textColor = .secondaryLabel
placeholderLabel.font = textView.font
textView.addSubview(placeholderLabel)
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
placeholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor,
constant: textView.textContainerInset.left + textView.textContainer.lineFragmentPadding),
placeholderLabel.topAnchor.constraint(equalTo: textView.topAnchor,
constant: textView.textContainerInset.top),
placeholderLabel.trailingAnchor.constraint(lessThanOrEqualTo: textView.trailingAnchor)
])
// 组合布局
contentStack.axis = .horizontal
contentStack.alignment = .center
contentStack.spacing = 8
addSubview(contentStack)
contentStack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStack.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
contentStack.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
contentStack.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
contentStack.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
])
// 左侧:附件、语音;中间:文本;右侧:发送
contentStack.addArrangedSubview(attachButton)
contentStack.addArrangedSubview(voiceButton)
contentStack.addArrangedSubview(textView)
contentStack.addArrangedSubview(sendButton)
// 文本高度约束(随内容变化)
textViewHeightConstraint = textView.heightAnchor.constraint(equalToConstant: minTextHeight)
textViewHeightConstraint.isActive = true
// 事件
attachButton.addTarget(self, action: #selector(onAttach), for: .touchUpInside)
voiceButton.addTarget(self, action: #selector(onVoice), for: .touchUpInside)
sendButton.addTarget(self, action: #selector(onSend), for: .touchUpInside)
updateUIState()
}
private func setupAccessibility() {
// 无障碍描述与顺序(从左到右)
attachButton.accessibilityLabel = NSLocalizedString("access_attach", comment: "添加附件")
attachButton.accessibilityTraits = .button
voiceButton.accessibilityLabel = NSLocalizedString("access_voice_toggle", comment: "语音输入")
voiceButton.accessibilityHint = NSLocalizedString("access_voice_toggle_hint", comment: "切换语音输入模式")
voiceButton.accessibilityTraits = .button
textView.accessibilityLabel = NSLocalizedString("access_message_input", comment: "消息输入框")
textView.accessibilityTraits = .allowsDirectInteraction
sendButton.accessibilityLabel = NSLocalizedString("access_send", comment: "发送")
sendButton.accessibilityTraits = .button
accessibilityElements = [attachButton, voiceButton, textView, sendButton]
}
// MARK: - Actions
@objc private func onAttach() {
delegate?.inputBarDidTapAttach(self)
}
@objc private func onVoice() {
// 简化为“切换语音模式”的开关逻辑(UI 切换由上层控制)
let isVoice = voiceButton.isSelected.toggle()
voiceButton.tintColor = isVoice ? .systemGreen : .label
delegate?.inputBarDidToggleVoice(self, isVoiceMode: isVoice)
}
@objc private func onSend() {
let trimmed = textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
delegate?.inputBarDidTapSend(self, text: trimmed)
}
// MARK: - UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
adjustTextViewHeight()
updateUIState()
delegate?.inputBar(self, didChangeText: textView.text)
updateSuggestionTrigger()
}
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
if sendOnReturn && text == "\n" {
onSend()
return false
}
return true
}
// MARK: - Helpers
private func adjustTextViewHeight() {
// 计算拟合高度
let target = CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude)
let fitting = textView.sizeThatFits(target).height
let clamped = min(max(fitting, minTextHeight), maxTextHeight)
textView.isScrollEnabled = fitting > maxTextHeight
if abs(textViewHeightConstraint.constant - clamped) > 0.5 {
textViewHeightConstraint.constant = clamped
invalidateIntrinsicContentSize()
// 通知系统更新 inputAccessoryView 高度
superview?.setNeedsLayout()
superview?.layoutIfNeeded()
}
}
private func updateUIState() {
let trimmed = textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
sendButton.isEnabled = !trimmed.isEmpty
sendButton.alpha = sendButton.isEnabled ? 1.0 : 0.5
}
private func updateSuggestionTrigger() {
let fullText = textView.text as NSString
let cursor = textView.selectedRange.location
guard cursor <= fullText.length else {
delegate?.inputBar(self, didTriggerSuggestion: nil)
return
}
// 提取光标前的 token
let prefix = fullText.substring(to: cursor)
if let token = prefix.split(whereSeparator: { $0 == " " || $0 == "\n" }).last {
if token.first == "@" {
let q = String(token.dropFirst())
delegate?.inputBar(self, didTriggerSuggestion: .mention(query: q))
return
} else if token.first == "/" {
let q = String(token.dropFirst())
delegate?.inputBar(self, didTriggerSuggestion: .command(query: q))
return
}
}
delegate?.inputBar(self, didTriggerSuggestion: nil)
}
// 对外方法
func insertSuggestionText(_ text: String, replacing trigger: SuggestionTrigger) {
guard let range = rangeForTrigger(trigger) else { return }
let ns = textView.text as NSString
let newText = ns.replacingCharacters(in: range, with: text + " ")
textView.text = newText
textViewDidChange(textView)
}
private func rangeForTrigger(_ trigger: SuggestionTrigger) -> NSRange? {
let ns = textView.text as NSString
let cursor = textView.selectedRange.location
let prefix = ns.substring(to: cursor)
guard let tokenRange = prefix.range(of: #"([@/][^\s\n]*)$"#, options: .regularExpression) else { return nil }
let lower = prefix.distance(from: prefix.startIndex, to: tokenRange.lowerBound)
let upper = prefix.distance(from: prefix.startIndex, to: tokenRange.upperBound)
return NSRange(location: lower, length: upper - lower)
}
func clearText() {
textView.text = ""
textViewDidChange(textView)
}
}
要点:
示例用 UITableView 作为建议面板,锚定在输入栏上方。
final class SuggestionView: UIView, UITableViewDataSource, UITableViewDelegate {
private let tableView = UITableView(frame: .zero, style: .plain)
var onSelect: ((String) -> Void)?
var items: [String] = [] {
didSet { tableView.reloadData() }
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemBackground
layer.cornerRadius = 8
layer.borderColor = UIColor.separator.cgColor
layer.borderWidth = 1 / UIScreen.main.scale
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.topAnchor.constraint(equalTo: topAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 44
}
required init?(coder: NSCoder) { fatalError() }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { items.count }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onSelect?(items[indexPath.row])
}
}
final class ChatViewController: UIViewController, ChatInputBarDelegate {
private lazy var inputBar = ChatInputBar()
private let suggestionView = SuggestionView()
// 埋点数据
private var inputStartTime: Date?
private let analytics = Analytics()
// 假数据
private let mentionCandidates = ["@Alice", "@Bob", "@Carol", "@David"]
private let commandCandidates = ["/giphy", "/shrug", "/me", "/topic"]
override var canBecomeFirstResponder: Bool { true }
override var inputAccessoryView: UIView? { inputBar }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// 输入栏委托
inputBar.delegate = self
// 建议视图
suggestionView.isHidden = true
view.addSubview(suggestionView)
suggestionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
suggestionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
suggestionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
suggestionView.bottomAnchor.constraint(equalTo: inputBar.topAnchor, constant: -8),
suggestionView.heightAnchor.constraint(lessThanOrEqualToConstant: 240)
])
suggestionView.onSelect = { [weak self] text in
guard let self = self else { return }
if let current = self.currentTrigger {
self.inputBar.insertSuggestionText(text, replacing: current)
self.hideSuggestions()
}
}
}
// MARK: - ChatInputBarDelegate
func inputBarDidTapSend(_ bar: ChatInputBar, text: String) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
showToast(NSLocalizedString("error_empty_message", comment: "内容不能为空"))
return
}
analytics.logSend(messageLength: trimmed.count, inputDuration: inputDuration())
// TODO: 发送消息到服务端
bar.clearText()
hideSuggestions()
}
func inputBarDidTapAttach(_ bar: ChatInputBar) {
// TODO: 打开系统文件/相册选择器
}
func inputBarDidToggleVoice(_ bar: ChatInputBar, isVoiceMode: Bool) {
// TODO: 切换到语音 UI(开始/结束录音等)
}
func inputBar(_ bar: ChatInputBar, didPasteImages images: [UIImage]) {
// TODO: 弹出预览模块(多选/单张确认)
// 示例:present 预览控制器
}
func inputBar(_ bar: ChatInputBar, didChangeText text: String) {
if inputStartTime == nil && !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
inputStartTime = Date()
}
}
private var currentTrigger: ChatInputBar.SuggestionTrigger?
func inputBar(_ bar: ChatInputBar, didTriggerSuggestion trigger: ChatInputBar.SuggestionTrigger?) {
currentTrigger = trigger
guard let trigger = trigger else {
hideSuggestions(); return
}
switch trigger {
case .mention(let q):
let data = q.isEmpty ? mentionCandidates : mentionCandidates.filter { $0.lowercased().contains(q.lowercased()) }
showSuggestions(data)
case .command(let q):
let data = q.isEmpty ? commandCandidates : commandCandidates.filter { $0.lowercased().contains(q.lowercased()) }
showSuggestions(data)
}
}
// MARK: - 建议面板显隐
private func showSuggestions(_ items: [String]) {
guard !items.isEmpty else { hideSuggestions(); return }
suggestionView.items = items
suggestionView.isHidden = false
}
private func hideSuggestions() {
suggestionView.isHidden = true
suggestionView.items = []
}
// MARK: - 轻提示
private func showToast(_ text: String) {
let label = PaddingLabel()
label.text = text
label.textColor = .white
label.backgroundColor = UIColor.black.withAlphaComponent(0.8)
label.layer.cornerRadius = 8
label.layer.masksToBounds = true
label.numberOfLines = 0
label.textAlignment = .center
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.bottomAnchor.constraint(equalTo: inputBar.topAnchor, constant: -16),
label.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, multiplier: 0.9)
])
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.warning)
UIView.animate(withDuration: 0.25, animations: { label.alpha = 1 }) { _ in
UIView.animate(withDuration: 0.25, delay: 1.5, options: [], animations: {
label.alpha = 0
}) { _ in label.removeFromSuperview() }
}
UIAccessibility.post(notification: .announcement, argument: text)
}
// 输入时长
private func inputDuration() -> TimeInterval {
defer { inputStartTime = nil }
guard let start = inputStartTime else { return 0 }
return Date().timeIntervalSince(start)
}
// 撤回示例(通常在消息气泡处触发)
func recallMessage(with id: String) {
// TODO: 调用后端撤回接口
analytics.logRecall(messageID: id)
}
}
// 带内边距的轻提示标签
final class PaddingLabel: UILabel {
var insets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
let s = super.intrinsicContentSize
return CGSize(width: s.width + insets.left + insets.right, height: s.height + insets.top + insets.bottom)
}
}
struct Analytics {
func logInputStart() {
// 可在首次输入时调用
}
func logSend(messageLength: Int, inputDuration: TimeInterval) {
// 发送事件:长度与输入耗时
}
func logRecall(messageID: String) {
// 撤回事件
}
}
"send_message_placeholder" = "发送消息……";
"access_attach" = "添加附件";
"access_voice_toggle" = "语音输入";
"access_voice_toggle_hint" = "切换语音输入模式";
"access_message_input" = "消息输入框";
"access_send" = "发送";
"error_empty_message" = "内容不能为空";
限制条件:
以上实现遵循 iOS Human Interface Guidelines 的基本建议,覆盖培训所需的主要概念与落地代码。基于此可进一步扩展消息类型、录音与附件流程、富文本等能力。
把分散、模糊的iOS界面元素描述,快速转化为可落地、可复用的技术文档。通过统一的结构与清晰的实现指引,帮助团队在原型评审、界面重构、代码走查和新人培训等场景中:
将原型或口头描述快速转为可执行技术文档,直接复用示例进行实现,减少反复沟通与返工。
统一组件实现规范,批量生成文档用于评审与基线管理,提升团队一致性与可维护性。
把交互稿中的组件说明转成技术化文档,提前验证可行性,缩短需求到上线的路径。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期